手機堆內存通常 **128KB-256KB** (JTWI 規範 256KB)。GC **非實時**、非確定性,**場景切換時新舊對象共存極易 OOM**。核心原則:**除基本類型外,所有對象用完顯式置 null,關鍵路徑手動調用 System.gc()**。
// ❌ 錯誤:新對象創建前舊對象未釋放,瞬間雙倍內存
GameLogic logic = new GameLogic();
logic = new GameLogic(); // 危險!
// ✅ 正確:先釋放再創建
if (logic != null) logic = null;
logic = new GameLogic();
protected void destroyApp(boolean unconditional) {
// 1. 關閉網絡連接
if (httpConn != null) { try { httpConn.close(); } catch (Exception ignored) {} httpConn = null; }
// 2. 釋放媒體播放器
if (player != null) { player.close(); player = null; }
// 3. 釋放圖片/圖像
if (bgImage != null) bgImage = null;
if (sprites != null) { for (Sprite s : sprites) s = null; sprites = null; }
// 4. 釋放集合
if (vector != null) { vector.removeAllElements(); vector = null; }
// 5. 關閉 RecordStore
if (rs != null) { try { rs.closeRecordStore(); } catch (Exception ignored) {} rs = null; }
// 6. 提示 GC
System.gc();
}
public static void logMemory(String tag) {
Runtime rt = Runtime.getRuntime();
long free = rt.freeMemory();
long total = rt.totalMemory();
System.out.println("[MEM] " + tag + " free=" + free + " total=" + total + " used=" + (total - free));
}
// 使用示例
logMemory("startApp-entry");
initUI();
logMemory("startApp-after-init");
loadLevel(1);
logMemory("startApp-after-load");
| 策略 | 實施要點 | 效果 |
|---|---|---|
| 尺寸裁剪 | 美術輸出精確屏幕尺寸 (如 176×220),禁止運行時縮放 | 減少 50-80% 像素內存 |
| 色深降級 | 無透明需求用 16 位 (RGB565);需透明用 8 位調色板 + 透明索引 | 每像素 2B → 1B |
| 圖集合併 | 多小圖合併為一張大圖,Graphics.drawRegion() 繪製區域 | 減少 Image 對象開銷、提升繪製批次 |
| 按需加載 | 關卡/場景制圖片,進場加載、退場釋放,不常駐內存 | 峰值內存大幅下降 |
| 避免 createImage(byte[]) | 優先用 Image.createImage("/path.png") 直接從 JAR 流式解碼 | 避免 byte[] 中間副本 |
| Mutable Image 復用 | 雙緩衝/離屏繪圖用 Image.createImage(w, h) 復用,不重複創建 | 減少 GC 壓力 |
適用於頻繁創建/銷毀的對象:Sprite、Particle、Bullet、Enemy、Vector 臨時容器。
public class ObjectPool {
private Vector freeList = new Vector();
private ObjectFactory factory;
public interface ObjectFactory { T create(); }
public ObjectPool(ObjectFactory factory, int initialSize) {
this.factory = factory;
for (int i = 0; i < initialSize; i++) freeList.addElement(factory.create());
}
public synchronized T acquire() {
if (freeList.size() > 0) {
T obj = (T) freeList.lastElement();
freeList.removeElementAt(freeList.size() - 1);
return obj;
}
return factory.create(); // 池耗盡臨時創建
}
public synchronized void release(T obj) {
resetObject(obj); // 重置狀態
freeList.addElement(obj);
}
protected void resetObject(T obj) { /* 子類實現具體重置邏輯 */ }
}
// 使用示例:子彈池
ObjectPool bulletPool = new ObjectPool<>(() -> new Bullet(), 20);
Bullet b = bulletPool.acquire(); b.init(x, y, dir); // 發射
// 碰撞/超界後
bulletPool.release(b); // 歸還池中
pngcrush/zopflipng 無損壓縮;音頻降低采樣率/比特率preverify 生成 StackMap,減少設備驗證開銷、加快啟動| 現場 | 根因 | 對策 |
|---|---|---|
| 場景切換瞬間 | 新舊場景對象共存 | 先釋放舊場景所有引用,再創建新場景;用 ObjectPool 復用通用對象 |
| 載入大圖/多圖 | 圖片解碼後佔用大量堆 | 圖集、按需加載、16位色深、Mutable Image 復用 |
| 網絡下載大數據 | byte[] 緩衝區過大 | 分塊流式處理,避免一次性讀入完整 byte[] |
| RMS 讀取大記錄 | getRecord 返回大 byte[] | 分頁讀取、或拆分多條記錄 |
| 字符串拼接循環 | String 不可變產生大量臨時對象 | 用 StringBuilder (MIDP 2.0+) 或自定義 CharArrayWriter |