| 類別 | 代表方法 | 觸發源 | 屬性 |
| 高層命令 | CommandListener.commandAction() | 用戶選擇軟鍵/菜單項 | 高級 API 專用 |
| 低層按鍵/指針 | Canvas.keyPressed/Released/Repeated()
Canvas.pointerPressed/Dragged/Released() | 物理按鍵/觸摸屏 | Canvas/CustomItem |
| 繪製回調 | Canvas.paint()
CustomItem.paint() | repaint() 請求 | 由系統調度 |
| 序列化執行 | Runnable.run() (via Display.callSerially()) | 應用程序主動請求 | 保證在事件隊列中執行 |
核心保證:所有 UI 回調嚴格序列化執行
規範原文:所有 UI 回調方法都是序列化的,所以它們永不會並行產生。任何 MIDP 實現都不能在前一個回調未返回的情況下調用另外一個回調。
序列化語義詳細解讀
- 互斥執行:任意時刻最多一個 UI 回調在運行
- 順序保證:事件隊列 FIFO,前一個返回後才調用下一個
- repaint 聚合:多次
repaint() 可能合併為單次 paint() 調用
- callSerially 排隊:
Runnable.run() 等待所有掛起的 repaint 處理完畢後執行
序列化受保護的回調方法完整清單
Canvas.hideNotify Canvas.keyPressed
Canvas.keyRepeated Canvas.keyReleased
Canvas.paint Canvas.pointerDragged
Canvas.pointerPressed Canvas.pointerReleased
Canvas.showNotify Canvas.sizeChanged
CommandListener.commandAction
CustomItem.getMinContentHeight CustomItem.getMinContentWidth
CustomItem.getPrefContentHeight CustomItem.getPrefContentWidth
CustomItem.hideNotify CustomItem.keyPressed
CustomItem.keyRepeated CustomItem.keyReleased
CustomItem.paint CustomItem.pointerDragged
CustomItem.pointerPressed CustomItem.pointerReleased
CustomItem.showNotify CustomItem.sizeChanged
CustomItem.traverse CustomItem.traverseOut
Displayable.sizeChanged
ItemCommandListener.commandAction
ItemStateListener.itemStateChanged
Runnable.run (via Display.callSerially)
關鍵特例:serviceRepaint() 打破序列化
規範:當 Canvas.serviceRepaint() 被調用時,Canvas.paint() 會被立即同步調用,並等待其返回。即使調用者本身就在一個回調中也一樣。
影響分析
- 重入風險:
paint() 可能在邏輯方法調用棧中被重入調用
- 狀態不一致:若邏輯代碼修改了繪製依賴的共享狀態,可能導致畫面撕裂
- 死鎖隱患:若
paint() 內部嘗試獲取邏輯線程持有的鎖
防禦性編程模式
// 邏輯線程中
synchronized(stateLock) {
updateGameState(); // 修改共享狀態
// 危險!若此處調用 serviceRepaint(),paint() 會嘗試獲取 stateLock
// canvas.serviceRepaint();
}
// 安全模式:分離狀態更新與繫製觸發
synchronized(stateLock) {
updateGameState();
// 僅複製繪製所需數據到不可變快照
renderSnapshot = createRenderSnapshot();
}
canvas.repaint(); // 異步請求,不會重入
// paint() 中
public void paint(Graphics g) {
RenderSnapshot snap;
synchronized(stateLock) { // 快速獲取鎖
snap = renderSnapshot; // 只讀快照
}
drawFromSnapshot(g, snap); // 無鎖繪製
}
定時器線程:並發的「特區」
規範:定時器事件並非 UI 事件,其回調方法可以和 UI 事件回調同時調用。同一 Timer 對象中的多個 TimerTask 則是序列化調用的。
並發風險場景
// 定時器線程
timer.schedule(new TimerTask() {
public void run() {
// 危險!與 UI 線程並發訪問 sharedData
sharedData.update();
}
}, 0, 100);
// UI 線程 (paint/keyPressed 等)
public void paint(Graphics g) {
// 並發讀取 sharedData → 數據競爭
g.drawString(sharedData.getValue(), 0, 0, 0);
}
三種線程安全方案
- Display.callSerially() 統一到 UI 線程(推薦)
timer.schedule(new TimerTask() {
public void run() {
Display.getDisplay(midlet).callSerially(new Runnable() {
public void run() {
sharedData.update(); // 安全:在 UI 線程執行
canvas.repaint();
}
});
}
}, 0, 100);
- volatile + 原子操作:適用於單一基本類型變量
- synchronized 細粒度鎖:鎖定時間極短,避免阻塞 UI 線程導致 ANR
最佳實踐總結
- UI 狀態單線程原則:所有 UI 相關狀態只在 UI 事件線程讀寫
- 定時器/網絡/IO 線程:僅負責數據準備,通過
callSerially() 將更新委派給 UI 線程
- 避免 serviceRepaint():除非極少數極高幀率遊戲場景,否則統一使用
repaint()
- 共享數據不可變快照模式:邏輯線程構建快照,UI 線程只讀渲染
- 鎖粒度最小化:
synchronized 僅保護極小臨界區,嚴禁在鎖內調用系統 API
精通事件模型與線程交互,是從「能跑」到「穩健」的分水嶺。
- 上一篇文章: KXML 解析器深度解析:J2ME 輕量級 XML 處理方案選型指南
- 下一篇文章: J2ME RMS 讀寫實戰:中文存儲解決方案與最佳實踐模板
Copyright ©
程式碼之家 All Rights Reserved