作者在開發 J2ME 遊戲時發現,Nokia S40 系列強制要求 JAR 體積不超過 30KB(含類字節碼、資源檔、MANIFEST.MF)。一個空類編譯後約 200+ 字節,十個類就佔 2KB+,這在現代開發中難以想像,卻是當年嵌入式 Java 的鐵律。
為了「正統」的遊戲架構,作者設計了以下類別:
| 類別 | 職責 | 預估大小 |
|---|---|---|
Sprite | 精靈數據層:位置、狀態、屬性介面 | ~400B |
SpriteView | 視圖層:動畫幀管理、圖片陣列分類 | ~600B |
ImageFrame | 單幀數據:圖片引用、碰撞盒、錨點、延遲 | ~300B |
SpritePool | 對象池:預創建實例、死亡回收、避免 GC 卡頓 | ~500B |
GameConfig | 全域配置常數、關卡參數 | ~300B |
GameWorld | 遊戲世界邏輯、實體管理、碰撞檢測 | ~800B |
GameMidlet | MIDlet 入口 | ~200B |
GameCanvas | 繪製迴圈、輸入處理 | ~400B |
Menu | 選單繪製與導航 | ~500B |
Map | 地圖數據解析、瓦片渲染 | ~700B |
Utils | 通用工具方法 | ~300B |
小計:~5.0KB 純字節碼,尚未計算資源檔(圖片、音效)、MANIFEST、META-INF。實際 JAR 易超 15KB+,留給遊戲內容極少。
Sprite 不持有圖片,SpriteView 專管渲染,便於同一數據多種表現(如不同解析度皮膚)SpriteView 所有幀,否則遊戲中卡頓new/GC 會導致幀率波動,SpritePool 預熱實例,平滑效能Sprite 僅持有 View 索引「這樣可以把內存中的利用率提至最高。」—— 經典的空間換時間、啟動換運行策略。
Sprite + SpriteView → Sprite 內嵌動畫狀態與幀索引ImageFrame 扁平化為陣列:int[] frameX, frameY, frameW, frameH, frameDelayGameConfig 常數內聯到使用處,或用 interface Config { int X=...; }(介面常數編譯後內聯,不產生類檔)Utils 方法改為 static 內聯到調用點(ProGuard 可自動內聯)
# proguard.cfg 關鍵配置
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
-optimizations !code/simplification/arithmetic
-keep public class * extends javax.microedition.midlet.MIDlet
-keep public class * extends javax.microedition.lcdui.Canvas
-keepclassmembers class * {
public static void main(java.lang.String[]);
}
效果:類名縮為 a、b...,移除未用代碼、調試資訊、內聯短方法,通常可減少 30%-50% 字節碼體積。
wtkbuild 針對不同裝置產出專用 JAR| 組件 | 原始 | ProGuard後 | 優化後 |
|---|---|---|---|
| 類字節碼 | 22 KB | 11 KB | 8 KB |
| 圖片資源 | 45 KB | 45 KB | 18 KB |
| 音效資源 | 18 KB | 18 KB | 6 KB |
| 關卡數據 | 12 KB | 12 KB | 4 KB |
| 總計 | 97 KB | 86 KB | 36 KB |
最終通過刪減非核心關卡、壓縮圖片 擠入 30KB 紅線。
雖然 J2ME 時代結束,但「受限資源下的工程權衡」永不過時:
「現在最基本的游戲核心實現就需要 7 個類,換算下來就有了平白失去了 1.4K 的空間。我真不知道 Java 怎麼實現這些的,但確實讓人窩火。」—— 作者的無奈,正是約束驅動創新的寫照。