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執行的錯誤!

即使兩台機器的規格支援有相同的規格也可能會有問題。

主要目標

我們在移植的主要目標就是我們每增加一個版本(“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世界是這樣。

有很多工具及技術可以幫助你,有商業化的產品及免費的產品,有些是有用的可以明顯地幫助你,沒有一個可以互相取代成為好的軟體工程。

感謝你看到這裡,很快就可以離開了,但最好的獎勵行動就是按一下幫我分享或留言,感恩喔~

點我分享到Facebook

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *