首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> J2ME

J2ME RMS 從入門到精通(一):RecordStore 核心機制、存儲限制與實戰測試工具

     編輯:J2ME

前言:為什麼 RMS 是 J2ME 持久化的唯一選擇?

功能機時代,文件系統(JSR 75)支援極有限,Record Management System (RMS) 成為 MIDP 規範強制實現的本地存儲標準。本文系統梳理 RMS 核心概念、API 細節、跨版本差異、裝置差異陷阱,並附贈實機存儲上限自動測試工具完整源碼。

核心概念解析

Record(記錄)

  • 本質:位元組陣列 byte[],無結構定義。
  • 大小:理論上限 64KB(單記錄),實際受裝置總配額限制。
  • 序列化責任:開發者自行用 DataOutputStream/ByteArrayOutputStream 寫入基本類型、字串、陣列。

RecordStore(記錄存儲)

  • 一個有序記錄集合,記錄 ID 從 1 遞增(刪除後不回收,除非 compact)。
  • 操作原子性:單次 addRecord/setRecord/deleteRecord 保證不損壞。
  • 命名空間:MIDlet Suite 級別共享,同套裝內所有 MIDlet 可互訪(MIDP 1.0/2.0 一致)。
  • 權限模型(MIDP 2.0 新增):AUTHMODE_PRIVATE/PUBLIC + WRITEABLE/READABLE,支援跨 Suite 共享。

存儲配額與 MIDlet-Data-Size

  • 規範要求:至少 8KB 非揮發性空間。
  • JAD/Manifest 必須宣告 MIDlet-Data-Size: 8192(單位 Byte),安裝時裝置檢查可用空間是否滿足。
  • 實測陷阱: 宣告值過大 → 安裝失敗;宣告值過小 → 運行時拋 RecordStoreFullException。建議:實測目標機型最大可用值 × 0.8 作為宣告值。

關鍵 API 速查表

方法說明備註
openRecordStore(name, createIfNecessary)開啟/建立同名存儲全 Suite 可見
closeRecordStore()關閉釋放鎖必須成對呼叫
deleteRecordStore(name)刪除整個存儲靜態方法
addRecord(data, offset, len)新增記錄回傳記錄 ID (1-based)
setRecord(id, data, offset, len)覆寫記錄ID 必須存在
getRecord(id, buffer, offset)讀取到指定緩衝區避免建立新 byte[]
getRecord(id)讀取並回傳新 byte[]簡單但頻繁呼叫產生 GC
deleteRecord(id)刪除單筆ID 不會重用
getNumRecords()當前記錄數
getSize()已用位元組數含索引開銷
getSizeAvailable()剩餘可用位元組不可靠! 見下文實測
enumerateRecords(filter, comparator, keepUpdated)枚舉/篩選/排序進階查詢利器
addRecordListener(listener)變更通知多 MIDlet 同步用

裝置差異血淚史:為什麼 getSizeAvailable() 不可信?

作者在 Nokia 6108 實測:getSizeAvailable() 回傳 >1MB,但寫入 40KB 即拋 RecordStoreFullException。

原因:

  1. 實作 Bug:回傳理論最大值而非實際剩餘。
  2. 檔案系統開銷:FAT 簇大小、目錄項、磨損均衡保留區。
  3. 共享配額:同 Suite 其他 MIDlet 佔用空間未扣除。

結論: 僅作參考,實際上限必須實機壓力測試。

實戰工具:RMSAnalyzer 自動測試最大存儲容量

原理:背景執行緒每 INTER 毫秒寫入 1KB 記錄,直到拋 RecordStoreFullException,記錄成功筆數即為上限(KB)。

核心類別

// MIDlet 入口
public class RMSAnalyzer extends MIDlet {
    private Display display; private CounterCanvas canvas; private Alert alert;
    protected void startApp() {
        display = Display.getDisplay(this);
        alert = new Alert("錯誤");
        try {
            int t = Integer.parseInt(getAppProperty("INTER")); // JAD 設定間隔
            canvas = new CounterCanvas(t, 1, this);
            display.setCurrent(canvas);
        } catch (RecordStoreException e) { showAlertError(e.getMessage()); }
    }
    public void showAlertError(String msg) { 
        alert.setString(msg); alert.setType(AlertType.ERROR); alert.setTimeout(3000);
        display.setCurrent(alert); 
    }
    protected void pauseApp() {} protected void destroyApp(boolean u) {}
}

// 畫面與測試邏輯
public class CounterCanvas extends Canvas implements CommandListener {
    private RMSModel model; private RMSAnalyzer midlet;
    private int interTime, counter; private boolean go = true;
    private Timer timer = new Timer();
    public static Command backCommand = new Command("退出", Command.EXIT, 3);

    public CounterCanvas(int t, int base, RMSAnalyzer m) throws RecordStoreException {
        interTime = t; counter = base; midlet = m; model = new RMSModel(base, m);
        addCommand(backCommand); setCommandListener(this);
        timer.schedule(new TimerTask() {
            public void run() {
                try {
                    model.writeRecord(1); // 寫入 1KB
                    counter++; repaint();
                } catch (RecordStoreFullException e) {
                    go = false; model.deleteRMS(); timer.cancel(); repaint();
                } catch (RecordStoreException e) {
                    model.deleteRMS(); timer.cancel(); midlet.showAlertError(e.getMessage());
                }
            }
        }, 1000, interTime);
    }

    protected void paint(Graphics g) {
        int w = getWidth(), h = getHeight();
        g.drawRect(w/10, h/2, w*4/5, 10);
        if (isColor()) g.setColor(0x8080FF);
        g.fillRect(w/10, h/2, counter, 10); // 進度條
        if (!go) g.drawString("最大值:" + counter + "K字節", 0, 0, Graphics.TOP|Graphics.LEFT);
    }
    public void commandAction(Command c, Displayable d) {
        if (c == backCommand) {
            try { model.deleteRMS(); midlet.destroyApp(false); midlet.notifyDestroyed(); }
            catch (Exception e) {}
        }
    }
}

// 資料模型:封裝 RecordStore 讀寫
public class RMSModel {
    public static final int K = 1024;
    private RecordStore rs; private int baseCount; private RMSAnalyzer midlet;
    public static final String NAME = "test";

    public RMSModel(int base, RMSAnalyzer m) throws RecordStoreException {
        baseCount = base; midlet = m;
        rs = RecordStore.openRecordStore(NAME, true);
        writeRecord(baseCount); // 初始寫入
    }
    public void writeRecord(int count) throws RecordStoreException {
        byte[] data = new byte[count * K];
        for (int i=0; i

JAD 設定關鍵

MIDlet-Data-Size: 8192
INTER: 100  // 每 100ms 寫入 1KB,約 10KB/s,平衡速度與系統負載

實測結果(文中數據)

  • 平台:Eclipse 3.0 + WTK 2.1 + Nokia 6108 真機
  • 結果:31 KB(遠低於 getSizeAvailable() 回傳的 1MB+)
  • 警告:嚴禁在模擬器測試,模擬器通常無配額限制,結果無參考價值。

MIDP 1.0 vs 2.0 共享機制差異

下一篇將詳細講解 MIDP 2.0 跨 Suite 共享實戰。

最佳實踐總結

  1. 序列化統一: 所有寫入用 DataOutputStream,讀取用 DataInputStream,嚴格對應 writeInt/readInt、writeUTF/readUTF、writeBoolean/readBoolean 等。
  2. 緩衝區複用: 高頻讀寫時使用 getRecord(id, buffer, offset) 避免大量 byte[] 分配觸發 GC。
  3. 異常分層處理: RecordStoreFullException(空間不足)→ 提示用戶清理/降級;RecordStoreNotFoundException(存儲不存在)→ 自動建立預設;其他 → 記錄日誌、顯示通用錯誤。
  4. 線程安全: 單執行緒遊戲通常無慮;若多 MIDlet 並發寫入,務必在應用層加 synchronized 或使用 RecordListener 協調。
  5. 備份策略: 關鍵資料(高分、付費記錄)建議雙份存儲(主存儲 + 備份存儲),啟動時校驗一致性。

結語

RMS 雖簡樸,卻是 J2ME 時代數據持久化的基石。理解其記錄導向、位元組透明、配額受限、裝置差異大四大特性,配合實機壓測驗證上限、統一序列化協議、異常分層處理,即可構建穩健的存儲層。文中 RMSAnalyzer 工具可直接納入專案測試流程,為 MIDlet-Data-Size 宣告提供科學依據。下一篇將深入 RecordStore 進階操作:枚舉器、比較器、過濾器、跨 Suite 共享實戰。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
特性MIDP 1.0MIDP 2.0
同 Suite 共享支援支援
跨 Suite 共享不支援支援(授權模式 + 讀寫控制)
建立參數openRecordStore(name, true)openRecordStore(name, true, authMode, writable)
授權模式—AUTHMODE_PRIVATE(僅同 Suite)/ AUTHMODE_PUBLIC(任意 Suite)
讀寫控制—true=可寫 / false=唯讀