Java移植

这一篇是我在Java Porting的中文翻译,网址在Java移植
为了备份,并转贴在此:

在手机的世界里,Java从来没有实现它的写一次到处执行的目标,手机会因其性能而有很大的不同。

  • 荧幕尺寸的不同,从96×65到800×480 (或不算Windows Mobile装置的话是到640×360)
  • Heap记忆体从200k到10Mb的差异
  • 执行速度会有最慢跟最快机种10倍不同的差异(资料来源:JBenchmark 2.0)
  • 有些机种没有JAR大小的限制,而有些则限制在64k以下。

开发人员被迫选择支援有限的设备、开发很简单的应用程式(最多机种所能接受的功能)或是开发多重的版本。

加上:

  • 支援不同的API差异
  • API执行的差异(但仍是规格内)
  • API执行的错误!

即使两台机器的规格支援有相同的规格也可能会有问题。

Contents

[hide]

主要目标

我们在移植的主要目标就是我们每增加一个版本(“SKU”),每个SKU下降的费用。

我不建议的移植策略

多源数

有一个意外热门的技术(一些主要的游戏发行公司会用)就是开发一个应用程式的版本给一个设备 (或最小性能的机器),然后复制几十份的原始码,每一份拷贝就可以修改来配合一特定的机器。

优点:

  • 每一个建置可以同时针对围绕在机器”问题”上的每台机器规格来订做
  • 适合一台机器的修正不会破坏另一台机器的建置
  • 你可以像建置的那样有很多的开发人员来降低专案的时间

一个最大的缺点:

  • 适合一台机器的修正不能帮助其他机器的建置,这样很难达到经济规模并且会增加整个专案的成本
    • 任何错误的原始码将重复数十次以上,产生大量的程式设计及测试的工作还有对产品的品质有威胁
    • 一个增加程式码移植性的开发人员(例如将”120″用”screenWidth / 2″取代)只帮助了他自己,却不能帮
    • 最新的规格变更会变得贵来执行

使用预处理器

这个热门的技术包含使用在C及C++的传统编译技术来维护一个单一原始码档案的一些程式码变异。

预处理器也提供一些其他的功能,像是巨集的展开(像”in-lining”一个函式呼叫及增进效率的招式)。

优点:

  • 单一源树减少维护的程式码数量;原始码的数量不会按建置的数量等比增加,较有经济规模
  • 工具也有
  • 在C社群里有完善的技术

缺点:

  • 当你使用这样的技术在两台或三台不同的机器上时看起来很好,但是很快的就会失控,并且假如你要支援30或40不同机器时会导致程式码不可读
  • 取决于预处理器使用,你的程式码可能不再是正确的Java(直到它被处理前) – 这会让你不能使用当前整合式工具的许多功能,像是语法检查、自动完成、程式码导航及重构
  • 保持你的程式码合Java的语法通常涵盖了很多的注解,取决于你所建置的版本 – 有这种程式码会使它很难运作
  • 切换版本通常表示要将你的程式码给取消注解,并注解你不要的程式码 – 这表示你改变每只原始档案时,就得建置一个不同的版本,而这会导致版本控制的梦魇
  • 进阶的预处理技术,像巨集的展开,伴随着即使是相当熟练的C程式设计师也会犯的程式设计陷阱 – Java的程式设计师可能没写过C,就有可能不了解这个陷阱

推荐的最佳作法

设计可维护的程式码!

在我们甚至想到用高明的工具来帮助我之前,显然最大的好处就是可以成为一位好的程式设计师,移植是软体维护的一个练习,假如程式是可维护的话成本会比较低。

  • 使用合理的变数及方法名称及注解(再一次使用升阳的惯例)
  • 撰写结构化程式码(并使用结构化的例外处理)
  • 不在程式码中使用字面常量,使用命名的常量(static final)

撰写跟荧幕尺寸无关的程式码

这个实际上是上一个项目的延伸,但需要值得一提。

使用api所提供的方法来取得荧幕的尺寸(透过Canvas.sizeChanged()事件),以及查询影像及字型的大小。

挑选执行时期的影像尺寸允许你只藉着修改作品(较大或较小的影像)来适应不同的荧幕尺寸,而不用改变任何程式码。

在荧幕上的位置项目相对于他们所属的位置,假如有些东西需要在底部,把它定位为相对于荧幕的底部,而不是顶部,记住:很多机器只是荧幕高度 上有差异,经典的例子像是摩托罗拉的机器,他们彼此的高度通常是相容的,有可能使用174或175像素的荧幕高度(全荧幕的画布),你不会想要针对这为小 的差异而产生两个分别的建置(两个大型的开发、测试、验证等等。)

假如要在画布上显示文字,写一个换行的方法,不要手动来分行!即使是两只相同型号却不同的手机也会有不同的内部字型大小(例如Nokia 7250i)。

开发最低规格

你需要事先决定你想要支援的机器种类,使用制造商的开发网站,或是像JBenchmark.com这样的网站,来检查heap记忆体大小及效率等功能,试着开发最少记忆体及最低效率的机器。

避免使用Nokia S60系列的机器最为主要开发机种,S60系列比其他主流机种有较好的效能及较多的记忆体,在较慢较少记忆体的机子上运作将会是你的恶梦,从较低规格的机器移植到较高规格的机企上会较容易。

不需要找”最差的”手机,你要专注开发一个产品,而不是围绕在一堆韧体错误的事上,Nokia、Sony Ericsson及Motorola的机器一般是最好的选择,因为他们够稳定,有很好的开发人员支援及相同制造商不同的机器有较高程度的相容。

使用Java Verified支援的表格来取得机器相容的想法,理想方法是可以从”lead device”这个栏位选择机器。

小心多执行绪

要小心使用过多的执行绪,不同的机器有可能使用不同执行绪排程演算,这可能在你不小心同步时因机器的不同而使多执行绪程式码产生不同的行为,记住一 次只执行一个执行绪,Java就不会有很多关于执行绪如何排成的规则让你去费思量,很少有手机可以像桌上电脑有复杂的多工及多执行绪的作业系统,所以要小 心不要指望你的手机可以工作得很顺利。

使用多执行绪来避免执行事件处理方法的工作,事件处理应尽快地传回,较快地传回错误会导致应用程式顿顿的或变得没有回应。

要小心在Canvas.paint()使用同步,特别是你正在使用serviceRepaints()的时候,这会在某些机器上不慎地造成死结。

其他不一致的行为

  • 从一个InputStream读取位元阵列,要一直检查传回值来看看真的读到多少位元。
  • CLDC-1.1: 不是所有的机种都有!
  • 转会字元到位元(反之亦然),小心使用”平台预设编码”的方法,像是String(byte[])或是String.getBytes(),他们的动作会因不同机器而异。
  • Timer及TimerTask在一些机器上也有问题,我建议避免使用。
  • LCDUI在不同的机器上看起来也不同,记住你不会知道Commands会出现在哪里,他们不需要被指定一个软键,因为某些机器有一个特别已经不用的”返回”键,这个键可能是使用BACK Commands。
  • 有些机器对较大的图档会很吃力,这不只是记忆体的考量,通常有可能是面积的关系,有些机器不能处理某个最大的宽度或高度的影像,举一个例,一个4096像 素宽1个像素高的影像也有可能错误,即使它的记忆须求很小,而影像128×32 (相同的像素值)却可能载入正常,一个经验法则,一个影像的两个方向最高的尺寸应该是:
    • 256 像素,对于荧幕宽度小于176像素宽的机器
    • 1024 像素,对于176像素宽或更宽的机器
  • platformRequest()在它作任何事之前在某些机器上须要程式跳出,检查它的传回值。
  • 电话进来时(或其他外来的事件)可能导致pauseApp()事件,或不是,假如你正显示一个画布,那么你可能会取得一个hideNotify()事件,在一些机器上,你可能不会得到任何事件,VM可能还继续在跑,或是被冻结起来直到通话结束。
  • startApp()可能会被呼叫一次以上,小心不要再一次重新初始化变数。

建构一个”设备属性”类别

机器的属性不能在执行期时被读取,建构一个类别来描述每一台机器。(这个类别应该包括荧幕的尺寸!!你可以在执行期取得荧幕尺寸!)

建构一个拥有所有属性的一般机器类别。

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;
}

特定机种的类别可以继承这一个,并提供他自己需要的值。

public class Device extends GenericDevice {
    public static final int MAX_PREFETCHED_SOUNDS = 3;
}

属性应该跟机器的特定功能特征有关,它们应该跟制造商、型号或作业系统有关,”NOKIA_SERIES_40″不是一个有用的事情要知道。

建构你所产生的每个版本的独立设备类别,分享单一的GenericDevice类别使它容易地新增新的属性,有一个预设值,且不需编辑每个单一的设备类别。

使用条件编译

你可以在Java这样做而不需预处理器的帮助。

if (Device.NEEDS_REGULAR_GC) {
    System.gc();
}

由于”Device.NEEDS_REGULAR_GC”是static final,他有基本型别,从常数初始化,其值在编译时会让编译器知道,假如值是”true”,编译器会忽略”if”,然后只是离 开”System.gc()”,假如值是false,编译器会忽略整个程式片段。

使用抽象

藉由建立自己的抽象层来应付不同的设备API,这是使用多源树精心定位的变化。

两个例子:

1. 假如你需要使用不同的声音播放器API(或甚至是不同的JSR135执行!),建立你自己的声音播放器介面,然后特定的设备来实作。

public interface SoundPlayer {
    public void loadSound(String name);
    public void setLooping(boolean looping);
    public void start();
    public void stop();
}

在你的JAR里将只有一个类别来实作这个介面,且较新版的Proguard能够侦测这个然后从建置清除介面。

2. 假如你需要继承相依于设备的Canvas、GameCanvas或FullCanvas,建构一个媒介类别(这个可以有不同特定设备的版本)。

所以将这个程式码变更:

public class MyCanvas extends
//#if MIDP2
    GameCanvas
//#elseif NOKIA
    com.nokia.mid.ui.FullCanvas
//#else
    Canvas
//#endif
{

你就会有:

public class MyCanvas extends DeviceSpecificCanvas {

这个DeviceSpecificCanvas类别看起来像:

public class DeviceSpecificCanvas extends Canvas {
    // might not even need any code
}

在每个类别大约150bytes的JAR里的顶部有一个per-class,然而像JAX、mBooster及新版的Proguard等工具可以合 并类别,DeviceSpecificCanvas 及 MyCanvas (上个例子)可以没有风险地被合并成功能性的程式码。

重复使用

重复使用的程式码已经通过移植周期,所以你不必再一次移植,假如你用切合实际的方法工作,每次的周期里会变得较有移植性也会有较少的错误。

假如你可以利用物件导向那么重复使用显然是最简单的,假如你不能,因为JAR的大小会限制阻止你有多一点的类别,那么你可以考虑”类别堆叠”的技术。

类别堆叠技术

不是最好的使用,但是我使用这个术语而且可以解释。

就像前面提到的,有一些不同的工具(像JAX及mBooster)可以合并类别,这样可以执行在:

  • 有一个类别是其他类别的父类别
  • 父类别是抽象类别(或者至少绝不实体化)
  • 他们没有相同命名的非私有成员或方法

假如这些都符合,那么类别可以不需增加heap的数量被合并成一个类别来拥有一个实体。

假如可以的话,那么就建置一个高且窄的类别阶层,一个类别宽且可以上到20或30个类别高(我不建议作到30类别以上的高),类别合并的程序可以将他们合成一个单一的大类别。

实际上,这种类似模组程式设计及静态连结,在C程式里是很常见的。

就像我说的这不是最好的实践,然而这是用挤压所有的程式码成一个大的类别较好方法(或是我至少有一次看到一个大的paint()方法及run()方法),它至少提供了些些重复使用、API抽象及帮忙可以在专案中工作增加开发人员的数量(不必一直合并改变)。

使用位元码工程

BCE是编译后用自动的方式来修改程式的技术,那就是.class档案通常可以用注射额外的位元码来修改,假如你听到人们提到 “AOP” (剖面导向程式设计),这个就是他们所讨论的。

BCE藉着让你注射标准的修正来处理API执行的错误是理想的,因为没有改变原始码,原始码可以保持可读性,并且你在其他的设备上的程式码也不会有风险。

修正常见已知的问题可以再使用元件的方式来封装,所以你就不需再考虑到修正的问题。

这个技术已经是移植工具像是Tira Wireless的”Jump”产品(一个商业化产品,现在就我所知已经挂了)的基础,另外有一个巧合的相似名称的开放原始码专案 “GUMP“也是用这个技术。

举个例来说,在全荧幕模式下至少会有一种设备会从Canvas.getHeight()传回错误值(它传回非全荧幕的高度),你需要取代任何呼叫这个方法有正确的值,你也需要在那台设备上对你所开发的每个游戏作处理,这个修正可以是这样简单的一件事:

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; }"
);

在这里”$_”是特别的地方用来表示我们将取代来自呼叫的传回值,修改过的程式码正像getHeight()函式被呼叫时所传回的值一样,会传回160。

请注意这个程式码不会在游戏或应用程式中,它是在JAR中作为BCE工具建置程序修改程式码时来执行,假如使用GUMP,这会变成”gumplets”函式库的一部分,用来修正特定的问题。

总结

用最低的成本来移植最多数的设备使你的投资报酬率最大化是明显的关键 – 至少在手机的Java世界是这样。

有很多工具及技术可以帮助你,有商业化的产品及免费的产品,有些是有用的可以明显地帮助你,没有一个可以互相取代成为好的软体工程。