|
In the old days programmers wrote their programs in assembly language, and were able to pack a lot of functionality into a few kilobytes of memory. Since then, technology advances such as object-oriented programming (OOP) have taken developers a long way from machine-level instructions and direct management of memory. The fundamental goal of OOP in particular is to increase the productivity of programmers through code reuse (inheritance for example), and not to produce applications that are compact.
Following the advice to "optimize later," most programmers focus on the logic, correctness, and reliability of applications, not their size and speed. Developers of applications that run on mobile devices with low power and a small memory footprint, however, can't afford to ignore efficiency. Hardware is expensive in the wireless world. Devices with less memory are much cheaper than devices with the same characteristics that have more memory, and they consume less power.
This article starts by describing reasons for reducing the size of wireless Java applications, discusses problems that arise out of using class libraries, then presents some useful techniques for reducing application size without sacrificing quality.
Motivation
Those who write software for big machines (and even notebook computers are huge compared to mobile devices) don't have to worry about memory, but developers of wireless Java applications must squeeze them into mobile devices with limited memory and compact processors. Making wireless applications smaller results in a number of benefits:
- Smaller applications download faster, and thus more cheaply.
- They take less time to load and run; acceptable start-up time is crucial.
- The smaller each application is, the more applications users can store on their devices.
- Large programs run up against preset size limits on some Java-enabled mobile devices. For example, some phones have a maximum size of 64K for an application JAR file and 512K total storage for all applications.
Effects of Using Class Libraries
Class libraries are meant to increase the productivity of software developers and the reliability of their code. They have some disadvantages, however.
One is that class libraries may contain many more features than an application requires, increasing its size unnecessarily.
You can use extraction tools to slim down applications that depend on class libraries. Such tools read the application's class files and the libraries they depend on to determine which classes, methods, and fields are actually used. They remove unused classes, methods, and fields, so the JAR file you produce contains only the functionality your application actually needs. Extraction tools have been known to reduce an app to less than 40% of its original size.
Another disadvantage of class libraries is that ones the application depends on may not be available in the deployment environment (on a mobile phone for example). This problem can be dealt with in two ways each of which poses other problems:
- The user can download and install the needed library, but this process is complex and error-prone; for example, users can easily install the wrong version of a library. While a proper packaging mechanism can handle versioning and dependency checking, the process is likely to require multiple transactions, and an error late in the process might invalidate earlier results if, for example, the device runs out of space before all dependencies are resolved.
- The developers can ship the class library that the application depends on. An example is an XML parser with the code. This approach, however, increases application size, the very result they're trying to avoid.
Class libraries also raise security issues. For example, JAR files from different vendors will have different signatures. This problem is one of the reasons shared libraries were not added in MIDP 2.0.
Techniques for Manual Reduction
There are many ways to reduce the size of your wireless Java application.
Keep it simple. Simplicity of design can yield robustness, speed, and efficient use of space.
Recompute rather than store. Recomputing a value rather than storing it for later retrieval can save space. As an example consider the following code fragment:
...
String s = "HelloMyDearFriend";
int len = s.length();
for(int i=0;i<len;i++) {
System.out.println(s);
}
...
|
This code uses the String.length() method to compute the length of the string before the loop, and thus only once. This approach is fine from a performance point of view, but creation of the len variable does increase the application's use of memory. To eliminate the variable you can recompute the string length on each loop iteration, as in the following rewrite:
...
String s = "HelloMyDearFriend";
for(int i=0;i<s.length();i++) {
System.out.println(s);
}
...
|
If you compile both versions, you'll find that the one that recomputes the length instead of storing it is smaller in size by several bytes. These bytes can add up....
- Choose an elegant data structure to store information. You should always consider your options for a data structure to store your information on the device.
 |
 |
 |
 |
 |
Note: It is all about trade-offs. Sometimes you must trade functionality or performance to gain space. You should always study the alternative before making a decision.
|
 |
 |
 |
Remove unnecessary classes. One way to "keep it simple" is to review your application design and eliminate classes that are not actually needed.
Reduce functionality and features. Is all the functionality you've planned actually needed?
Avoid inner classes. Many programmers use inner classes, to implement event listeners for example. Convenient, but behind the scenes the compiler generates special instructions to allow the inner class to access the outer class's private data. Consider the following snippet of code for an event listener implemented as an anonymous inner class:
public class MyMIDlet extends MIDlet {
// code for the MIDlet
// ...
// Anonymous inner class
setCommandListener(new CommandListener() {
public void commandAction(Command c, Displayable d) {
// implementation;
// may access MyMIDlet's private fields
}
});
}
|
Redefining the inner class to be a separate helper class would eliminate the extra overhead.
Avoid deep inheritance hierarchies. Although the benefits of inheritance (such as software reuse, increased reliability, and code sharing) are great, nothing is without cost of some sort, including inheritance. Programs must use memory and time to construct fields inherited from a base class even when the derived class makes no use of them. When a method is invoked and it is not part of the derived class, its immediate superclass will be searched; if that class doesn't define the method, its superclass will be searched, and so on. All that extra message passing adds bulk to the bytecode and steals processor time. If a class doesn't use most of the resources it inherits from the base class, it's better to reimplement the resources it does use, instead of inheriting them.
Avoid using inheritance when it isn't necessary. For example, if you wish to use multiple threads, implement the Runnable interface rather than extending the Thread class.
Use short names for classes, methods, and fields. Savings from abbreviating any one name aren't dramatic, but they add up. An automated way to get shorter names is to use obfuscation tools. Although their primary intent is to make it harder for a competitor to reverse-engineer your application by decompiling your .class files into easily read .java files, obfuscators also reduce code size by shortening names. There are several obfuscators available, but you may want to try RetroGuard, which is available for free. You can use this obfuscator from the J2ME Wireless Toolkit.
 |
 |
 |
 |
 |
Tip: In the build cycle, always perform obfuscation before preverification. The preverify tool, which is shipped with the J2ME Wireless Toolkit, preprocesses classes for use by the KVM without changing class names.
Preverification is itself a way to reduce demand on the limited resources of mobile devices. In the J2SE Java virtual machine, the class verifier is responsible for rejecting invalid class files. A Java virtual machine supporting the CLDC must be able to reject invalid class files as well, but the class verification process is expensive and time-consuming, not ideal for small, resource-constrained devices. The designers of the J2ME virtual machine lightened the load on the mobile device by moving most of the verification work onto the developer's desktop, where the class files are compiled, or onto a server machine, where applications are downloaded. This preliminary, off-device verification leaves the device responsible only for running a few checks on the class files, to ensure that they have in fact been verified, and that they're still valid.
|
 |
 |
 |
Take advantage of resource-processing tools that reduce PNG resolution and color depth. Many devices are capable of displaying only a few colors per pixel, so creating images with many colors per pixel may increase your app's memory footprint without adding any value to it. You may also want to reduce the dimensions of the image.
Don't include unnecessary images. Why use memory for images that don't enhance your application?
Eliminate access methods. Get and set methods can cause your application to run more slowly than if it accessed member variables directly, and they increase program size. Once again, though, it is all about trade-offs.
Refactor. As applications evolve over time, they often become less space-efficient. Sometimes the best way to save space is to refactor the application, with space-efficiency as a high-level design goal.
Minimize object creation. Object creation consumes memory and processor cycles, and leads to object destruction, which further reduces performance. Look for ways to reuse existing objects rather than create new ones.
As one example, instead of creating a return object inside a method, consider passing to it a reference to an object that already exists in the calling code, and modify that object's values.
For another example, consider this code snippet:
...
int len = record.length;
try {
for(int i=0; i<len; i++) {
MyObject obj = new MyObject();
// do something with obj
}
} catch(Exception e) {
e.printStackTrace();
}
...
|
This code creates and destroys a new instance of MyObject every time the loop iterates. You can avoid this object churning by moving the object creation outside the loop. A more efficient way to rewrite the code above would be to create the object outside the try block and reuse it as follows:
...
int len = record.length;
MyObject obj = new MyObject();
try {
for(int i=0; i<len; i++) {
// do something with obj
}
} catch(Exception e) {
e.printStackTrace();
}
...
|
By reusing a single object instead of creating many, the program uses less memory and reduces garbage collection. To avoid memory leaks, however, you should clear all the elements in any recycled object before reusing it. To do that, just add a clear() method (as in the Collection interface in J2SE) or a reset() method.
Avoid string concatenation. It is well known that concatenating objects with the + operator causes object creation and subsequent garbage collection, and thus chews up both memory and processor time. It is more efficient to use StringBuffer. Note, however, that merely switching from concatenation to use of StringBuffers is not always sufficient because of the way arrays are reallocated. Inefficiencies arise, chiefly because the default character buffer for StringBuffer is 16, and when the buffer is full a new one (usually twice the size of the original) must be allocated. Once the content is copied to the new one, the old buffer is released. This frequent reallocation can be avoided if you create a StringBuffer big enough to hold the longest string you're likely to store.
Conclusion
Several vendors of Java-enabled mobile devices have imposed physical constraints on the size of each application and the total memory available for applications. Application size affects download and start-up times. This article presented several techniques for reducing the size of wireless Java applications.
Due to the increasing complexity of wireless applications and time-to-market pressures, manual compaction can be difficult. Therefore there is a growing need for automated compaction techniques and other tools that will help developers reduce the size of their wireless Java applications.
For More Information
- J2ME Wireless Toolkit
- RetroGuard
Acknowledgments
Special thanks to Gary Adams of Sun Microsystems, whose feedback helped me improve this article.
About the Author:
Qusay H. Mahmoud provides Java consulting and training services. He has published dozens of articles on Java, and is the author of Distributed Programming with Java (Manning Publications, 1999) and Learning Wireless Java (O'Reilly & Associates, 2002).
Back To Top
|
|