In the world of mobile, Java never manages to achieve its write once, run everywhere objective. Mobile phones differ wildly in capability.
Developers are forced to choose between supporting a limited subset of devices, developing a very simple application (lowest common denominator), or developing multiple versions.
Add to that:
Even supporting two devices with the same specification can be a problem.
Our primary objective in porting is that, with each additional version ("SKU") we produce, the cost per SKU drops.
A surprisingly popular technique (used within some of the major game publishers) is to develop a single version of the application, for one device (or a small subset of devices), then make dozens of copies of the source code. Each copy will then be modified to suit a specific device.
Advantages:
One big disadvantage:
This very popular technique involves using conditional compilation techniques used in C and C++ to maintain a number of code variants in a single source code file.
Pre-processors can also provide a number of other facilities, such as macro expansion (a trick for "in-lining" a method call and increasing performance).
Advantages:
Disadvantages:
Before we even think about clever tools that can help us, by far the biggest benefit comes just from being a good programmer. Porting is an exercise in software maintenance. It's cheaper if the code is maintainable.
This is really an extension of the previous item, but one that deserves special mention.
Make use of all the methods the API provides for receiving the size of the screen (through the Canvas.sizeChanged() event), and for querying the size of images and fonts.
Picking up image sizes at runtime allows you to adapt to different screen sizes just by changing the artwork (for bigger or smaller images), without needing any code changes.
Position items on the screen relative to where they belong. If something needs to be at the bottom, position it relative to the bottom of the screen, not the top. Remember: many devices vary only slightly in screen height. A classic example is Motorola devices, which are often highly compatible with each other, but may use a screen height of 174 or 175 pixels (fullscreen Canvas). You don't want to have to produce two separate builds to meet this one tiny difference (with two lots of development, testing, certification, etc.)
If you're displaying text in a Canvas, write a word-wrapping method. Don't break the text into lines manually! Even two different handsets of the same model can have different internal font sizes (Nokia 7250i, for example).
You need to make some decisions in advance about what range of devices you want to support. Use the manufacturer developer sites, or sites like JBenchmark.com, to check for heap sizes and performance figures. Try to develop for the device with the least memory and the slowed performance.
Avoid using a Nokia Series 60 as your primary development device. S60s have more performance and more memory than the vast majority of devices, and you can end up with an application that is nightmare to get working on slower, lower memory devices. It is easier to port from a lower-spec device to a higher-spec device.
Don't necessarily work on the "worst" phone. You want to focus on developing a product, not on working around a plethora of firmware bugs. Nokia, Sony Ericsson and Motorola devices generally make the best choices, as they are reasonably stable, have good developer support, and have high levels of compatibility with other devices from the same manufacturer.
Use the Java Verified table of supported devices to get an idea about device compatibility. Ideally, choose devices from the "lead device" column.
Be careful about using many threads. Different devices are likely to use different thread scheduling algorithms, which can make multi-threaded code behave very differently across devices if you're not careful about synchronization. Remember that only one thread can actually run at a time, and Java does not have as many rules about how thread scheduling should work as you might think. Few phones have sophisticated multi-tasking, multi-threaded operating systems like your desktop computer has, so be careful not to expect your phone to work quite so smoothly.
Do use multi-threading to avoid performing lots of work in an event handler method. Event handlers should return as quickly as possible. Failure to return quickly may result in applications that stutter or become unresponsive.
Be careful about using synchronization in Canvas.paint(), especially if you are using serviceRepaints(). You can inadvertantly cause deadlocks on some devices.
For properties of a device that cannot be read from the device at runtime, create a class to describe each device. (This class should not include the screen size!! You can get that at runtime!)
Create a generic device class, with all properties.
abstract class GenericDevice {
// the number of sounds that the device can have in the prefetched state at once
public static final int MAX_PREFETCHED_SOUNDS = 1;
// true if the device suffers from heap fragmentation and needs regular System.gc()
public static final boolean NEEDS_REGULAR_GC = false;
// true if RMS access on this device is very slow (10 seconds or more)
public static final boolean RMS_IS_SLOW = false;
}
A device-specific class extends this, and provides its own values as needed.
public class Device extends GenericDevice {
public static final int MAX_PREFETCHED_SOUNDS = 3;
}
Properties should relate to specific, functional characteristics of devices. They should not refer to manufacturer, model, or operating system. "NOKIA_SERIES_40" is not a useful thing to know.
Create a separate Device class for each version you will produce. Sharing a single GenericDevice class makes it easy to add a new property, with a default value, without having to edit every single Device class.
You can do this in Java without the aid of a pre-processor.
if (Device.NEEDS_REGULAR_GC) {
System.gc();
}
Since "Device.NEEDS_REGULAR_GC" is a static final, it has a primitive type, and it is initialized from a constant, its value is known to the compiler at compile-time. If the value is "true", the compiler will ignore the "if", and leave just the "System.gc()". If the value is false, the compiler will ignore this entire code fragment.
Cope with different device APIs by creating your own abstraction layer. This is a carefully-targetted variation on using multiple source trees.
Two examples:
1. If you need to use different sound player APIs (or even different implementations of JSR135!), create your own sound-player interface, then device-specific implementations.
public interface SoundPlayer {
public void loadSound(String name);
public void setLooping(boolean looping);
public void start();
public void stop();
}
You'll only have one class in your JAR that implements the interface, and newer versions of Proguard are able to detect this and eliminate the interface from the build.
2. If you need to extend Canvas, GameCanvas or FullCanvas depending on device, create an intermediate class (for which you will have different, device-specific versions).
So, instead of this:
public class MyCanvas extends
//#if MIDP2
GameCanvas
//#elseif NOKIA
com.nokia.mid.ui.FullCanvas
//#else
Canvas
//#endif
{
You just have:
public class MyCanvas extends DeviceSpecificCanvas {
A DeviceSpecificCanvas class looks something like:
public class DeviceSpecificCanvas extends Canvas {
// might not even need any code
}
There is a per-class overhead in a JAR of around 150 bytes per class. However, tools like JAX, mBooster and newer versions of Proguard can merge classes. DeviceSpecificCanvas and MyCanvas (in the example above) can be merged without any risk to the functionality of the code.
Re-use code that has already been through a porting cycle, so you don't have to port it again. If you're working in a sensible way, each time around the cycle it will become more portable, and have fewer bugs.
Re-use is obviously easiest if you can exploit object orientation. If you can't, because JAR size constraints stop you having more than a few classes, then you can consider the "class stacking" technique.
Not a best practice, but I've used the term and it warrants explanation.
As mentioned before, there are various tools (such as JAX and mBooster) that can merge classes. This works best when:
If these are met, then the classes can be merged into a single class, without increasing the amount of heap required to hold an instance.
It is possible, then, to build a tall, thin class hierarchy, one class wide and up to 20 or 30 classes tall (I don't recommend making it taller than 30 classes). The process of class merging can then reduce them all down to a single, large class.
In effect, this mimmics modular programming and static linking, as is common with C programs.
As I say, this is not a best practice. However, it is better than trying to squeeze all your code into one huge class (or, as I've seen on at least one occasion, one large paint() method and one large run() method). It does at least provide for some re-use, API abstraction, and helps increase the number of developers who can work on the project (without having to merge changes all the time).
BCE is a technique for modifying the program in an automated way, after compilation. That is, the .class files themselves are modified, usually by injecting additional byte code. If you hear people referring to "AOP" (Aspect Oriented Programming), this is usually what they are talking about.
BCE is ideal for coping with bugs in API implementations, by enabling you to inject a standard fix. Because there are no source code changes, the source code remains readable, and you don't risk breaking the code for other devices.
Fixes for common, known issues can be encapsulated in re-usable components, so that you never have to think about fixing that issue again.
This technology has been the basis of porting tools such as Tira Wireless's "Jump" product (a commercial product, which is now, to my knowledge, defunct), and also of the coincidentally similarly-named, open source project "GUMP".
For example, at least one device that returns an incorrect value from Canvas.getHeight() when in fullscreen mode (it returns the non-fullscreen height). You need to replace any call to this method with the correct value, and you need to do this for every game you develop on that device. This fix can be simply a matter of:
replaceCalls(allClasses(),
// this is the signature for the calls we want to replace
"javax.microedition.lcdui.Canvas.getHeight()I",
// this represents the code we're going to inject to replace each method call
"{ $_ = 160; }"
);
Here, "$_" is a special place-holder to represent the return value from the call we're replacing. The modified code will function exactly as if getHeight() had been called, and 160 had been returned.
Note that this code does not go in the game or application. It is executed by the BCE tool as part of the build process, modifying the code in the JAR. If using GUMP, for example, this becomes part of a library of "gumplets", each of which fixes a specific issue.
Porting to the largest number of devices at the lowest possible cost is obviously key to maximizing the return on your investment - in the mobile Java world at least.
There are many tools and techniques that can help you. There are commercial products, and there are free products. Some of these are useful, and can help you significantly. None of them is an alternative to good software engineering.
No related wiki articles found