Sun Java Solaris Communities My SDN Account Join SDN
 
Wireless Tech Tips

J2ME Tech Tips: February 26, 2002

 

Tech Tips archive

February 26, 2002

WELCOME to the Java Developer Connection (JDC) Java 2 Platform, Micro Edition (J2ME) Tech Tips, for February 26, 2002. This issue covers:

The J2ME Tech Tips are written by Eric Giguere (http://www.ericgiguere.com), an engineer at iAnywhere Solutions, inc. Eric is the author of the book "Java 2 Micro Edition: Professional Developer's Guide" and co-author of the book "Mobile Information Device Profile for Java 2 Micro Edition," both books in John Wiley & Sons' Professional Developer's Guide series.

Pixel

OPTIMIZING J2ME APPLICATION SIZE

If there's anything that differentiates J2ME applications from their J2SE counterparts, it's the limited environment in which they run. The primary limitation in many J2ME systems is the amount of memory available to store and run applications. Many current MIDP devices, for example, limit application sizes to 50K or less -- a far cry from the multi-megabyte applications possible in server-based J2SE environments. It's very easy to run up against these limits. So in this J2ME Tech Tip, you'll learn some techniques for minimizing application size. You'll use these techniques to reduce the size of the following MIDlet which does nothing but display a text field and beep whenever its contents change:

package com.j2medeveloper.techtips;

import javax.microedition.lcdui.*;

public class BeforeSizeOptimization extends 
                                         BasicMIDlet {

    public static final Command exitCommand =
                         new Command( "Exit",
                                    Command.EXIT, 1 ); 

    public BeforeSizeOptimization(){
    }

    protected void initMIDlet(){
        getDisplay().setCurrent( new MainForm() );
    }

    public class MainForm extends Form {
        public MainForm(){
            super( "MainForm" );

            addCommand( exitCommand );
            append( textf );

            setCommandListener( new CommandListener(){
                public void commandAction( Command c,
                                       Displayable d ){
                    if( c == exitCommand ){
                        exitMIDlet();
                    }
                }
              }
            );

            setItemStateListener( 
                              new ItemStateListener() {
                public void itemStateChanged( 
                                           Item item ){
                    if( item == textf ){
                        AlertType.INFO.playSound(
                                        getDisplay() );
                    }
                }
              }
            );
        }

        private TextField textf =
                  new TextField( "Type anything", null,
                                 20, 0 );

    }
}

Although a MIDlet serves as the example, the techniques described here apply to size optimization for any J2ME profile.

Note that the MIDlet class shown above depends on the following convenience class:

package com.j2medeveloper.techtips;

import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;

public abstract class BasicMIDlet extends MIDlet {

    private Display display;

    public BasicMIDlet(){
    }

    protected void destroyApp( boolean unconditional )
                    throws MIDletStateChangeException {
        exitMIDlet();
    }

    public void exitMIDlet(){
        notifyDestroyed();
    }

    public Display getDisplay(){ return display; }

    protected abstract void initMIDlet();

    protected void pauseApp(){
    }

    protected void startApp()
                    throws MIDletStateChangeException {
        if( display == null ){ 
            display = Display.getDisplay( this );
            initMIDlet();
        }
    }

}

When packaged using the J2ME Wireless Toolkit, the sample MIDlet is just over 4K bytes in size.

The first step in reducing size is to remove unnecessary classes by pruning the application's functionality. Are all the features of your application really necessary? Can your users get by without all the "bells and whistles?" Build the most minimal version of the application. Notice that the sample MIDlet is pretty minimal already.

The second step is to look closely at any inner classes defined by the application, particularly anonymous classes. Remember that each class file has a certain amount of overhead associated with it. Even the most trivial class has overhead:

    public class foo {
        // nothing here
    }

Compile this class and you get a class file that is almost 200 bytes in size. A common use for an anonymous class, for example, is to implement an event listener. The sample MIDlet defines two listeners this way. An easy optimization to make, then, is to make the main MIDlet class (which is not an optional or unnecessary class) implement the CommandListener and ItemStateListener interfaces, and move the listener code there. Remember that multiple objects can use the same listeners. Use the arguments passed to the commandAction and itemStateChanged methods to distinguish between them if necessary.

Inner classes also bloat code other ways, because the compiler must generate special variables and methods to allow an inner class to access its enclosing class' private information. Read the original inner class specification for more details.

The third step is to maximize your use of pre-installed classes. For example, in CLDC-based profiles don't build your own set of collection classes. Use the built-in Hashtable and Vector classes and work around their limitations. The same goes for form creation in MIDP applications. The sample MIDlet defines a Form subclass to create its main form, but it could just as easily create it directly:

    mainForm = new Form( "MainForm" );
    mainForm.addCommand( okCommand );
    mainForm.setCommandListener( listener );

There's no right or wrong answer, it's simply something to consider.

The fourth step is to collapse your application's inheritance hierarchies. You might have factored common code into one or more abstract classes, a recommended technique for object-oriented design that promotes code reuse between applications. It might contradict what you've learned, but it might make more sense to simplify the inheritance hierarchy. This is especially true if your abstract class -- which might be from another project -- is only subclassed once. The sample MIDlet extends the BasicMIDlet class, for example, but the two are easily combined into a single class.

The fifth step is to shorten the names of your packages, classes, methods and data members. This might seem silly, but a class file holds a lot of symbolic information. Shorten the names of things and you'll reduce the size of the class file. The savings won't be dramatic, but they can add up when spread over several classes. Package names are particularly ripe for shortening. Because MIDP applications are completely self-contained, you can avoid package names completely -- there is no chance of conflict with other classes on the device. The sample MIDlet could be moved out of the com.j2medeveloper.techtips package.

Note that name shortening is not normally something you want to do by hand. Instead, use a tool called an "obfuscator" to do it for you. An obfuscator's primary purpose is to "hide" the code for an application by making it nearly unreadable when decompiled. A side effect of this process is to shrink the size of the application. That's because the hiding is done primarily by renaming methods and data members. There is an open source obfuscator called RetroGuard that is available for free from http://www.retrologic.com, and there are a number of commercial packages also available. (When obfuscating code for CLDC-based profiles, remember to do the obfuscation before the preverification step, otherwise the obfuscation will invalidate the preverification data stored in the class file.)

Finally, look closely at array initialization. (The sample MIDlet doesn't do any array initialization, but this is an important step for applications that do array initialization.) When compiled, an array initialization statement such as this:

    int arr[] = { 0, 1, 2, 3 };

actually produces code that behaves more like this:

    arr[0] = 0;
    arr[1] = 1;
    arr[2] = 2;
    arr[3] = 3;

To see this, use the javap tool that ships with the Java 2 SDK to disassemble the byte code to a class (use the -c option). You might be unpleasantly surprised at what you see, especially if what you expected was a straight binary copy of constant data. Two alternative approaches are to (1) encode the data into a string and decode it into an array at runtime, or (2) store the data as a binary file packaged with the application and make it accessible at runtime using the class loader's getResourceAsStream method.

These are just guidelines, and not every step mentioned here makes sense for every J2ME application. However, most of them do apply to the sample MIDlet. The optimized version of the MIDlet looks like this:

import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;

public class ASO extends MIDlet
                 implements CommandListener,
                            ItemStateListener {

    private Display   display;
    private Form      mainForm;
    private TextField mainFormTF =
                  new TextField( "Type anything", null,
                                 20, 0 );

    public static final Command exitCommand =
                         new Command( "Exit",
                                     Command.EXIT, 1 ); 

    public ASO(){
    }

    public void commandAction( Command c,
                               Displayable d ){
        if( c == exitCommand ){
            exitMIDlet();
        }
    }

    protected void destroyApp( boolean unconditional )
                    throws MIDletStateChangeException {
        exitMIDlet();
    }

    public void exitMIDlet(){
        notifyDestroyed();
    }

    public Display getDisplay(){ return display; }

    protected void initMIDlet(){
        mainForm = new Form( "MainForm" );
        mainForm.addCommand( exitCommand );
        mainForm.setCommandListener( this );
        mainForm.setItemStateListener( this );
        mainForm.append( mainFormTF );

        getDisplay().setCurrent( mainForm );
    }

    public void itemStateChanged( Item item ){
        if( item == mainFormTF ){
            AlertType.INFO.playSound( getDisplay() );
        }
    }

    protected void pauseApp(){
    }

    protected void startApp()
                    throws MIDletStateChangeException {
        if( display == null ){ 
            display = Display.getDisplay( this );
            initMIDlet();
        }
    }
}

Pixel

OBJECT SERIALIZATION IN CLDC-BASED PROFILES

The Connected Limited Device Configuration (CLDC) does not support object serialization or reflection. This means that unlike Mobile Information Device Profile (MIDP), there is no built-in way to persist objects into a byte stream for CLDC-based profiles. Applications that need to persist objects must build their own persistence mechanisms. That's true whether the need is to write the objects to a persistent storage facility like MIDP's Record Management System (RMS) or to send them across a network connection. It's hard to build general persistence support that works with arbitrary objects. However, writing task-specific persistence mechanisms is much simpler and arguably more suited to the constraints of the CLDC platform.

Persistence is easier to implement with the cooperation of the class that is to be persisted. At the most basic level it's just a matter of defining an interface like this:

import java.io.*;

/**
 * A simple interface for building persistent objects on
 * platforms where serialization is not available.
 */

public interface Persistent {

    /**
     * Called to persist an object.
     */

    byte[] persist() throws IOException;

    /**
     * Called to resurrect a persistent object.
     */

    void resurrect( byte[] data ) throws IOException;
}

Then you make your class implement the interface.

Consider a simple class describing an employee:

// Non-persistent version

public class Employee {
    private int      employeeID;
    private String   firstName;
    private String   lastName;
    private int      managerID;

    public Employee( int employeeID, String firstName,
                     String lastName, int managerID ){
        this.employeeID = employeeID;
        this.firstName = firstName;
        this.lastName = lastName;
        this.managerID = managerID;
    }

    public int getID() { return employeeID; }

    public String getFirstName() {
        return firstName != null ? firstName : ""; 
    }

    public String getLastName() {
        return lastName != null ? lastName : "";
    }

    public int getManagerID() { return managerID; }
}

The persistent version adds a null constructor and implements the Persistent interface:

// Persistent version

import java.io.*;

public class Employee implements Persistent {
    private int      employeeID;
    private String   firstName;
    private String   lastName;
    private int      managerID;

    public Employee(){
    }

    public Employee( int employeeID, String firstName,
                     String lastName, int managerID ){
        this.employeeID = employeeID;t
        this.firstName = firstName;
        this.lastName = lastName;
        this.managerID = managerID;
    }

    public int getID() { return employeeID; }

    public String getFirstName() {
        return firstName != null ? firstName : ""; 
    }

    public String getLastName() {
        return lastName != null ? lastName : "";
    }

    public int getManagerID() { return managerID; }

    public String toString() {
        StringBuffer b = new StringBuffer();
        b.append( '{' );
        b.append( employeeID );
        b.append( ',' );
        b.append( firstName );
        b.append( ',' );
        b.append( lastName );
        b.append( ',' );
        b.append( managerID );
        b.append( '}' );
        return b.toString();
    }

    public byte[] persist() throws IOException {
        ByteArrayOutputStream bout = 
                           new ByteArrayOutputStream();
        DataOutputStream      dout = 
                          new DataOutputStream( bout );

        dout.writeInt( getID() );
        dout.writeUTF( getFirstName() );
        dout.writeUTF( getLastName() );
        dout.writeInt( getManagerID() );
        dout.flush();

        return bout.toByteArray();
    }

    public void resurrect( byte[] data ) 
                                   throws IOException {
        ByteArrayInputStream bin = 
                      new ByteArrayInputStream( data );
        DataInputStream      din = 
                            new DataInputStream( bin );

        employeeID = din.readInt();
        firstName = din.readUTF();
        lastName = din.readUTF();
        managerID = din.readInt();
    }
}

The persistence is actually accomplished using the DataOutputStream and DataInputStream classes, which allow you to easily write and read Java primitives and strings. With the Employee class, then, an employee can be persisted at any point, like this:

    Employee emp = .....; // an employee instance

    try {
        byte[] persisted = emp.persist();
    }
    catch( java.io.IOException e ){
        // do something here
    }

Restoring the employee is done like this:

    byte[] persisted = ....; // presistence info
    Employee emp = new Employee();
    
    try {
        emp.resurrect( persisted );
    }
    catch( java.io.IOException e ){
        // do something here
    }

Note that the Persistent interface uses byte arrays, not byte streams, for storing the persistence information. This is a deliberate choice. In MIDP applications you're most likely to persist objects to the RMS, and the RMS APIs are array-based, not stream-based. That said, it would be just as simple to write stream-based persistence methods.

Things are more complicated, of course, if your objects contain references to other persistent objects. The example above avoids this issue by storing the manager ID in the Employee class. A more natural implementation is to use a reference to another Employee object. This complicates the persistence. That's because, instead of persisting a single object, you need to persist a sequence of objects, possibly with cross-references to each other. Writing the code to deal with this scenario is non-trivial. You need to traverse the object graph, ensure that each object gets written at most once, and deal with cycles in the graph. It's better to avoid these issues by making persisted objects completely self-contained, and use keys (unique identifiers) to link objects together.

Classes that don't implement the Persistent interface can also be persisted, but only if they expose enough information in their public interface. The java.util.Vector class, for example, can be persisted using static methods defined on a helper class:

import java.io.*;
import java.util.*;

public class VectorHelper {

    private static final int NULL = 0;
    private static final int INTEGER = 1;
    private static final int STRING = 2;
    private static final int PERSISTENT = 3;

    public static byte[] persist( Vector v )
                            throws IOException {
        ByteArrayOutputStream bout =
                          new ByteArrayOutputStream();
        DataOutputStream      dout =
                          new DataOutputStream( bout );

        int n = v.size();
        dout.writeInt( n );

        for( int i = 0; i < n; ++i ){
          Object o = v.elementAt( i );
            if( o instanceof String ){
                dout.writeByte( STRING );
                dout.writeUTF( (String) o );
            } else if( o instanceof Integer ){
                dout.writeByte( INTEGER );
                dout.writeInt( 
                            ((Integer) o).intValue() );
            } else if( o instanceof Persistent ){
                dout.writeByte( PERSISTENT );
                dout.writeUTF( 
                              o.getClass().getName() );
                byte[] data = 
                            ((Persistent) o).persist();
                dout.writeInt( data.length );
                if( data.length > 0 ){
                   dout.write( data );
                }
            } else if( o == null ){
                dout.writeByte( NULL );
            } else {
                throw new IOException( 
                "Cannot persist " +"object of type " +
                   o.getClass().getName() );
            }
        }

        dout.flush();

        return bout.toByteArray();
    }

    public static Vector resurrect( byte[] persisted )
                            throws IOException {
        ByteArrayInputStream bin =
                    new ByteArrayInputStream( 
                                          persisted );
        DataInputStream      din =
                    new DataInputStream( bin );

        Vector v = new Vector();
        int n = din.readInt();

        for( int i = 0; i < n; ++i ){
            int type = din.readByte();
            if( type == NULL ){
                v.addElement( null );
            } else if( type == INTEGER ){
                v.addElement( new
                       Integer( din.readInt() ) );
            } else if( type == STRING ){
                v.addElement( din.readUTF() );
            } else if( type == PERSISTENT ){
                String cname = din.readUTF();
                int    len = din.readInt();
                byte[] tmp = new byte[ len ];

                din.readFully( tmp );

                try {
                    Class cl = Class.forName( cname );
                    Object o = cl.newInstance();
                    ((Persistent) o).resurrect( tmp );
                    v.addElement( o );
                }
                catch( IOException e ){
                    throw e;
                }
                catch( Exception e ){
                    throw new IOException( 
                         "Exception " +e.toString() );
                }
                } else {
                   throw new IOException( "Unknown " +
                   "type " + type );
            }
        }

        return v;
  
    }
}

For brevity, the helper class only handles vectors whose elements are of type Integer or String, or that implement the Persistent interface. It can be easily extended to support other core types such as Long and Boolean.

Note that the VectorHelper class is fairly generic. So it has to write type information as part of the persistence data in order to correctly recreate the vector's contents. Most vectors typically hold elements of the same type. In that case you might be better off treating the vector as an array of objects, and simply writing out the element count and then each element, without all the extra type information.

Here is a simple MIDlet that uses the classes introduced in this tip to persist information to an RMS record store. The first time you run the MIDlet, it saves objects into the record store. The next run of the MIDlet reads the objects and displays their values.

import java.io.*;
import java.util.*;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.rms.*;

public class PersistenceTest extends MIDlet
                 implements CommandListener {

    private Display   display;
    private Form      mainForm;

    public static final Command exitCommand =
                         new Command( "Exit",
                                     Command.EXIT, 1 ); 

    public PersistenceTest(){
    }

    public void commandAction( Command c,
                               Displayable d ){
        if( c == exitCommand ){
            exitMIDlet();
        }
    }

    protected void destroyApp( boolean unconditional )
                    throws MIDletStateChangeException {
        exitMIDlet();
    }

    public void exitMIDlet(){
        notifyDestroyed();
    }

    public Display getDisplay(){ return display; }

    protected void initMIDlet(){
        String str;
        RecordStore rs = null;

        try {
            rs =
                RecordStore.openRecordStore( 
                                       "test2", true );

            if( rs.getNumRecords() == 0 ){
                str = store( rs );
            } else {
                str = retrieve( rs );
            }
        }
        catch( Exception e ){
            str = e.toString();
        }

        mainForm = new Form( "MainForm" );
        mainForm.addCommand( exitCommand );
        mainForm.setCommandListener( this );
        mainForm.append( str );

        getDisplay().setCurrent( mainForm );
        
        if( rs != null ){
            try {
                rs.closeRecordStore();
            }
            catch( RecordStoreException e ){
            }
        }
    }

    protected void pauseApp(){
    }

    protected void startApp()
                    throws MIDletStateChangeException {
        if( display == null ){ 
            display = Display.getDisplay( this );
            initMIDlet();
        }
    }

    private String store( RecordStore rs )
                           throws RecordStoreException,
                                  IOException {
        Employee emp = new Employee( 
                                1, "Joe", "Clark", 0 );
        byte[]   data = emp.persist();

        rs.addRecord( data, 0, data.length );

        Vector v = new Vector();
        v.addElement( new Integer( 99 ) );
        v.addElement( "The Great One" );
        v.addElement( new Employee( 99, "Wayne",
                                    "Gretzky", 1 ) );

        data = VectorHelper.persist( v );
        rs.addRecord( data, 0, data.length );

        return "Wrote " + emp.toString() + " and " +
               v.toString();
    }

    private String retrieve( RecordStore rs )
                           throws RecordStoreException,
                                  IOException {

        byte[]   data = rs.getRecord( 1 );
        Employee emp = new Employee();

        emp.resurrect( data );

        data = rs.getRecord( 2 );
        Vector v = VectorHelper.resurrect( data );

        return "Read " + emp.toString() + " and " +
               v.toString();
    }
}

Pixel

IMPORTANT: Please read our Terms of Use, Privacy, and Licensing policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html

- FEEDBACK

Comments? Send your feedback on the J2ME Tech Tips to:
jdc-webmaster@sun.com

- SUBSCRIBE/UNSUBSCRIBE

- To subscribe, go to the subscriptions page, choose the newsletters you want to subscribe to and click "Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".
- To use our one-click unsubscribe facility, see the link at the end of this email:

- ARCHIVES

You'll find the J2ME Tech Tips archives at: http://java.sun.com/jdc/J2METechTips/index.html

- COPYRIGHT

Copyright 2002 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA.

This document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html

J2ME Tech Tips February 26, 2002

Sun, Sun Microsystems, Java, Java Developer Connection, J2ME, and J2SE, are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.