记录管理系统(Record Management System,RMS)是移动信息设备描述(Mobile Information Device Profile,MIDP)的一个主要子系统,是一种应用程序编程接口(API),为 MIDP 应用程序提供本地的、基于设备的数据持久存储功能。在各种 MIDP 设备大行其道的今天,RMS 是唯一可以实现本地数据存储的工具——极少设备支持传统的文件系统。可以设想一下,彻底地了解 RMS,对于编写任何依靠持久性本地数据的程序来说,是多么至关重要。

本文是一系列文章的第一篇。这些文章将探究 RMS,以及解答有关它在 MIDP 应用程序中使用情况的较重要的问题,例如,与外部数据源(如关系数据库)相互作用的问题。开始我们要讨论的是 RMS 提供一些什么内容,并编写几个简单的 RMS 调试诊断程序。

关键概念

首先,要理解记录管理系统的一些关键概念。

记录

顾名思义,RMS 是用来管理记录的系统。一条记录是一个单一的数据项。RMS 没有对放入记录的内容加以限制:记录可以是数字、字符串、数组和图像——任何一段连续字节可以描述的事物。如果您能将数据编译成二进制代码,并且具备相应 的解码手段,那么您就能将其存入一条记录中,当然受系统分配的容量大小的限制。

许多 RMS 的初学者都对记录这个词感到困惑。“字段在什么地方?”他们奇怪系统是如何将单一记录细分为分散的数据序列。答案很简单:在RMS中,记录不包含任何字段。或者说得更准确些,一条记录只包含一个大小可变的二进制字段。这样,解释记录内容的任务就完全依靠应用程序来完成。RMS 只提供存储和惟一标识符。虽然这样分工导致应用程序趋于复杂化,但是它使得 RMS 小巧灵活——这是作为 MIDP 子系统的一项重要属性。

在 API 级别中,记录是简单的字节数组。

记录存储

记录存储是记录的有序集合。记录不是独立的实体:每一条记录必须从属于一个记录存储,而且所有的记录存取都通过记录存储来实现。事实上,记录存储保证记录读写的自动运行,不会发生数据损坏。

在创建一条记录时,记录存储为其分配一个惟一的标识符。该标识符是称作记录 ID 的整数。加入记录存储的第一条记录的记录 ID 是 1,第二条的记录 ID 是 2,依此类推。记录 ID不是索引:记录删除后不会对现存的记录进行重新编号,也不会影响后面记录的记录 ID 值。

在 MIDlet 套件中使用名称来标记记录存储。一个记录存储名可以包括 1 到 32 位 Unicode 字符,而且在创建该记录存储的 MIDlet 套件中该名称必须惟一。在 MIDP1.0 中,记录存储不能被不同的 MIDlet 套件共享。在MIDP2.0 里面,则允许 MIDlet 套件与其他套件共享同一个记录存储,其中此记录存储由 MIDlet 套件的名称,销售商的名称以及记录存储自己的名称共同加以识别。

记录存储也具有时间戳记和版本信息,所以应用程序能够知道记录存储最后被修改的时间。为了进一步紧密跟踪,应用程序可以注册监听程序以便及时获知记录存储何时被修改。

在 API level 中,用 javax.microedition.rms.RecordStore 类的实例来代表记录存储。所有的RMS类和接口在 javax.microedition.rms 包中都又定义。

RMS 外观

在我们看一些代码之前,先回顾一下几个有关 RMS 的关键信息。

存储容量限制

基于记录的数据存储的有效存储空间总量,因设备的不同而不同。MIDP 规范要求设备为持久数据存储,保留至少 8k 非易失性存储空间。该规范没有对个别记录的大小加以限制,可是空间限制会随着设备的不同而发生改变。RMS 提供多种方式决定单条记录的大小,记录存储的总容量,以及保留多少空间用于数据存储。记住持久性存储器是共享的,是珍贵的资源,因此请节约使用。

任何使用 RMS 的 MIDlet 套件,都应该通过在 JAR 的清单(manifest)文件和应用程序描述符中设置 MIDlet 数据大小(MIDlet-Data-Size)属性,指定所需的数据存储空间的最小字节数。不要使设置的值大于必须的容量,因为设备有可能拒绝安装那些数据存储要求超过有效空间的 MIDlet 套件。如果该属性缺失,那么设备将会假定此MIDlet 套件不需要数据存储空间。实际上,大多数设备允许应用程序超过它们规定的空间要求,但是请不要依赖这种方式。

需要指出,某些 MIDP 的实现需要定义其他的一些有关存储需求的属性——请查阅设备文档了解细节。

速度

对持久性存储器操作所需的时间,通常都要比对易失性(非持久性)存储器进行相同操作所需的时间更长。特别是写数据,在某些平台上写数据会持续很长时间。为了提高性能,不是从 MIDlet 事件线程中来完成 RMS 操作,而是利用缓存频繁访问易失性存储器中的数据以保证用户接口反应迅速。

线程安全性

RMS 操作具有很高的线程安全性,但对任何共享资源而言,各个线程仍必须协调对同一记录存储的数据读写操作。这种协调要求需要采用在不同 MIDlets 中运行的线程,因为在同一个 MIDlet 套件中记录存储是共享的。

异常

一般说来,RMS API 中的方法除了会抛出诸如 java.lang.IllegalArgumentException 之类的标准运行时异常之外,还会抛出一个或多个经检查的异常。RMS 异常都包括在 javax.microedition.rms 包内:

  • InvalidRecordIDException 在由于记录 ID 无效而无法执行某个操作时抛出。
  • RecordStoreFullException 在记录存储中没有足够的可用空间时抛出。
  • RecordStoreNotFoundException 在应用程序试图打开一个不存在的记录存储时抛出。
  • RecordStoreNotOpenException 应用程序试图访问已经关闭的记录存储时抛出。
  • RecordStoreException 是其他 4 个异常的总集,并且在出现它们未包括的一般错误时抛出。

注意,为了精简,这一系列文章中的代码里面的异常处理都被简化或者省略了。

使用 RMS

剩下的文章内容讲述的是使用 RMS API 时对记录的一些基本操作。某些操作贯穿于一个工具类 RMSAnalyzer 的开发中,用于记录存储分析。可以将 RMSAnalyzer 用作自己项目的调试诊断程序。

记录存储探究

可以通过调用 RecordStore.listRecordStores() 获得 MIDlet 套件中记录存储的列表。这个静态方法返回一个字符串数组,其中每一个字符串代表该 MIDlet 套件中的一个记录存储的名称。如果没有记录存储,则返回为 null。

方法 RMSAnalyzer.analyzeAll() 使用 listRecordStores() 为套件中的每个记录存储调用 analyze()

public void analyzeAll(){
String[] names = RecordStore.listRecordStores();

for( int i = 0;
names != null && i < names.length;
++i ){
analyze( names[i] );
}
}

注意,此数套件标识的只是自身 MIDlet 套件的记录存储。也就是说创建它们的那一个套件。MIDP 规范无论如何不包括列出其他 MIDP 套件记录存储的功能。在 MIDP 1.0 中,自身套件以外的记录存储是根本看不到的。在 MIDP 2.0,自己的套件可以将记录存储指派为可共享的,但其他 MIDlet 套件只有在知道其名称后才能使用它。

打开和关闭记录存储

RecordStore.openRecordStore() 用于打开一个记录存储,也可以用于有选择地创建一个记录存储。该静态方法返回一个 RecordStore 对象的实例,如下面的 RMSAnalyzer.analyze() 版本所示:

public void analyze( String rsName ){
RecordStore rs = null;

try {
rs = RecordStore.openRecordStore( rsName, false );
analyze( rs ); // call overloaded method
} catch( RecordStoreException e ){
logger.exception( rsName, e );
} finally {
try {
rs.closeRecordStore();
} catch( RecordStoreException e ){
// Ignore this exception
}
}
}

openRecordStore() 的第二个参数表示若该记录存储不存在,是否应该创建它。假如使用的是 MIDP 2.0,并且希望 MIDlet 打开由其他套件建立的记录存储,就可以用以下形式的 openRecordStore()

...
String name = "mySharedRS";
String vendor = "EricGiguere.com";
String suite = "TestSuite";
RecordStore rs =
RecordStore.openRecordStore( name, vendor, suite );
...

销售商和套件的名称,必须与 MIDlet 套件的清单(manifest)文件和应用程序描述符所定义的名称相匹配。

完成对记录存储的操作之后,调用 RecordStore.closeRecordStore() 将其关闭,与 analyze() 方法一样。

RecordStore 实例在一个MIDlet 套件中是惟一的:它一旦被打开,以后用同一名称调用 openRecordStore() 将会返回相同的对象引用。这个实例被该 MIDlet 套件的所有 MIDlet 共享。

每一个 RecordStore 实例都会跟踪它被打开的次数。除非调用相同次数的 closeRecordStore(),否则记录存储不会被真正关闭。在记录存储关闭之后使用它会抛出 RecordStoreNotOpenException。

创建记录存储

调用 openRecordStore() 并将第二个参数设为真来创建一个专用的记录存储:

...
// Create a record store
RecordStore rs = null;

try {
rs = RecordStore.openRecordStore( "myrs", true );
} catch( RecordStoreException e ){
// couldn't open it or create it
}
...

要执行记录存储的一次性初始化,请在打开记录存储后,立即检查 getNextRecordID() 是否等于 1:

if( rs.getNextRecordID() == 1 ){
// perform one-time initialization
}

或者,要在某个记录存储一旦为空就重新初始化该记录存储,请检查 getNumRecords() 返回的值:

if( rs.getNumRecords() == 0 ){
// record store is empty, re-initialize
}

要创建共享的记录存储(只在 MIDP 2.0 中),请使用 openRecordStore() 的四个参数变量:

int     authMode = RecordStore.AUTHMODE_ANY;
boolean writable = true;

rs = RecordStore.openRecordStore( "myrs", true,
authMode, writable );

当第二个参数为真且该记录存储已经不存在,则最后两个参数控制它的授权模式以及可写性。授权模式决定其他 MIDlet 套件能否访问该记录存储。有两种可能的模式分别为 RecordStore.AUTHMODE_PRIVATE(只有自身 MIDlet 套件有权访问)和 RecordStore.AUTHMODE_ANY(任何 MIDlet 套件都有权访问)。可写性标记决定其他 MIDlet 套件可否修改此记录存储——如果为假,它们只能从该记录存储中读取数据。

注意自身 MIDlet 套件能够使用 RecordStore.setMode 随时更改记录存储的权限模式和可写性:

rs.setMode( RecordStore.AUTHMODE_ANY, false );

实际上,最好用 AUTHMODE_PRIVATE 建立一个共享记录存储,并且在初始化之后将其发布。

添加与更新记录

还记得记录是字节数组吧。使用 RecordStore.addRecord() 添加新的记录到打开的记录存储:

...
byte[] data = new byte[]{ 0, 1, 2, 3 };
int recordID;

recordID = rs.addRecord( data, 0, data.length );
...

通过将第一个参数设为 null 来添加一个空记录。第二个和第三个参数指定了数组的开始偏移量及从该偏移量开始要存储的总字节数。添加成功后返回新纪录的 ID,否则抛出类似于 RecordStoreFullException 的异常。

可以用 RecordStore.setRecord() 随时更新记录:

...
int recordID = ...; // some record ID
byte[] data = new byte[]{ 0, 10, 20, 30 };

rs.setRecord( recordID, data, 1, 2 );
// replaces all data in record with 10, 20
...

不能批次添加或者更新记录:您必须在内存中作为字节数组的形式构建整个记录,而且使用一次调用来添加或更新记录。

您能通过调用 RecordStore.getNextRecordID(),找出下一个记录标识符调用 addRecordStore() 时会返回什么值。所有当前的记录标识符都会小于这个值。

在第二篇文章中我们将着眼于将对象和其他数据转化为字节数组的策略。

读取记录

可以利用 RecordStore.getRecord() 的两种形式中的一种读取记录。第一种形式分配合适大小的字节数组并将记录数据复制到里面:

...
int recordID = .... // some record ID
byte[] data = rs.getRecord( recordID );
...

第二种形式是从指定的偏移量开始,将数据复制到预分配的数组中并返回所复制的字节数量:

...
int recordID = ...; // some record ID
byte[] data = ...; // an array
int offset = ...; // the starting offset

int numCopied = rs.getRecord( recordID, data, offset );
...

数组必须有足够的容量来容纳数据,否则会抛出 java.lang.ArrayIndexOutOfBoundsException。使用 RecordStore.getRecordSize() 返回的值指定一个足够大的数组。实际上,etRecord() 的第一种形式相当于:

...
byte[] data = new byte[ rs.getRecordSize( recordID ) ];
rs.getRecord( recordID, data, 0 );
...

第二种形式在对于一系列记录的迭代操作时,进行最小化存储器分配工作很有帮助。例如,可以使用它和 getNextRecordID()及 getRecordSize() 一起来执行在记录存储中对所有记录的强制搜索:

...
int nextID = rs.getNextRecordID();
byte[] data = null;

for( int id = 0; id < nextID; ++id ){
try {
int size = rs.getRecordSize( id );

if( data == null || data.length < size ){
data = new byte[ size ];
}

rs.getRecord( id, data, 0 );

processRecord( rs, id, data, size ); // process it
} catch( InvalidRecordIDException e ){
// ignore, move to next record
} catch( RecordStoreException e ){
handleError( rs, id, e ); // call an error routine
}
}
...

但是,更好的方法是使用 RecordStore.enumerateRecords() 对记录进行迭代操作。我的第三篇文章将讨论 enumerateRecords() 的使用。

删除记录和记录存储

RecordStore.deleteRecord() 删除记录:

...
int recordID = ...; // some record ID
rs.deleteRecord( recordID );
...

一旦记录被删除,任何使用它的企图都会抛出 InvalidRecordIDException

您用 RecordStore.deleteRecordStore() 删除记录存储:

...
try {
RecordStore.deleteRecordStore( "myrs" );
} catch( RecordStoreNotFoundException e ){
// no such record store
} catch( RecordStoreException e ){
// somebody has it open
}
...

记录存储只有当其未打开时才能被删除,并且只能由自身 MIDlet 套件中的 MIDlet 来执行。

其他操作

剩下还有几个 RMS 操作,它们都是属于 RecordStore 类的方法:

  • getLastModified() 返回上次修改记录存储的时间,与 System.currentTimeMillis() 返回的格式相同。
  • getName() 返回记录存储的名称。
  • getNumRecords() 返回记录存储中的记录数。
  • getSize() 以字节为单位返回记录存储的总大小。总量包括所有记录的总大小以及系统执行记录存储所需的额外开销。
  • getSizeAvailable() 返回记录存储能增加的可用字节数。注意实际的可用大小可能比存储个别记录花费的额外开销要小。
  • getVersion() 返回记录存储的版本号。版本号是大于零的正整数,每次记录存储更改后都会增加。

MIDlet 也能使用 addRecordListener() 注册监听程序来跟踪记录存储的改变,然后可以用 removeRecordListener() 撤销注册。我将会在第三篇文章中讨论这些监听程序。

RMSAnalyzer

第一篇以 RMSAnalyzer 类——记录存储的分析器——的源代码作为结束。下面是分析记录存储的代码:

...
RecordStore rs = ...; // open the record store
RMSAnalyzer analyzer = new RMSAnalyzer();
analyzer.analyze( rs );
...

默认情况下,分析程序转至 System.out stream 流,请看下面的代码:

=========================================
Record store: recordstore2
Number of records = 4
Total size = 304
Version = 4
Last modified = 1070745507485
Size available = 975950

Record #1 of length 56 bytes
5f 62 06 75 2e 6b 1c 42 58 3f _b.u.k.BX?
1e 2e 6a 24 74 29 7c 56 30 32 ..j$t)|V02
5f 67 5a 13 47 7a 77 68 7d 49 _gZ.Gzwh}I
50 74 50 20 6b 14 78 60 58 4b PtP k.x`XK
1a 61 67 20 53 65 0a 2f 23 2b .ag Se./#+
16 42 10 4e 37 6f .B.N7o
Record #2 of length 35 bytes
22 4b 19 22 15 7d 74 1f 65 26 "K.".}t.e&
4e 1e 50 62 50 6e 4f 47 6a 26 N.PbPnOGj&
31 11 74 36 7a 0a 33 51 61 0e 1.t6z.3Qa.
04 75 6a 2a 2a .uj**
Record #3 of length 5 bytes
47 04 43 22 1f G.C".
Record #4 of length 57 bytes
6b 6f 42 1d 5b 65 2f 72 0f 7a koB.[e/r.z
2a 6e 07 57 51 71 5f 68 4c 5c *n.WQq_hL\
1a 2a 44 7b 02 7d 19 73 4f 0b .*D{.}.sO.
75 03 34 58 17 19 5e 6a 5e 80 u.4X..^j^?
2a 39 28 5c 4a 4e 21 57 4d 75 *9(\JN!WMu
80 68 06 26 3b 77 33 ?h.&;w3

Actual size of records = 153
-----------------------------------------

这种格式在使用 J2ME 无线工具包测试时很方便。为了在实际设备上测试,可以将分析结果发送到串口或者直接通过网络传输至 servlet。您可以通过定义一个自己的类来达到这个目的。这个类实现 RMSAnalyzer.Logger 接口并将该类的一个实例传输到 RMSAnalyzer 构造程序。

附在本文之后的是一个称为 RMSAnalyzerTest 用于示范分析程序使用的 J2ME 无线工具包项目:

package com.ericgiguere;

import java.io.*;
import javax.microedition.rms.*;

// Analyzes the contents of a record store.
// By default prints the analysis to System.out,
// but you can change this by implementing your
// own Logger.

public class RMSAnalyzer {

// The logging interface.

public interface Logger {
void logEnd( RecordStore rs );
void logException( String name, Throwable e );
void logException( RecordStore rs, Throwable e );
void logRecord( RecordStore rs, int id,
byte[] data, int size );
void logStart( RecordStore rs );
}

private Logger logger;

// Constructs an analyzer that logs to System.out.

public RMSAnalyzer(){
this( null );
}

// Constructs an analyzer that logs to the given logger.

public RMSAnalyzer( Logger logger ){
this.logger = ( logger != null ) ? logger :
new SystemLogger();
}

// Open the record stores owned by this MIDlet suite
// and analyze their contents.

public void analyzeAll(){
String[] names = RecordStore.listRecordStores();

for( int i = 0;
names != null && i < names.length;
++i ){
analyze( names[i] );
}
}

// Open a record store by name and analyze its contents.

public void analyze( String rsName ){
RecordStore rs = null;

try {
rs = RecordStore.openRecordStore( rsName, false );
analyze( rs );
} catch( RecordStoreException e ){
logger.logException( rsName, e );
} finally {
try {
rs.closeRecordStore();
} catch( RecordStoreException e ){
// Ignore this exception
}
}
}

// Analyze the contents of an open record store using
// a simple brute force search through the record store.

public synchronized void analyze( RecordStore rs ){
try {
logger.logStart( rs );

int lastID = rs.getNextRecordID();
int numRecords = rs.getNumRecords();
int count = 0;
byte[] data = null;

for( int id = 0;
id < lastID && count < numRecords;
++id ){
try {
int size = rs.getRecordSize( id );

// Make sure data array is big enough,
// plus add some for growth

if( data == null || data.length < size ){
data = new byte[ size + 20 ];
}

rs.getRecord( id, data, 0 );
logger.logRecord( rs, id, data, size );

++count; // only increase if record exists
}
catch( InvalidRecordIDException e ){
// just ignore and move to the next one
}
catch( RecordStoreException e ){
logger.logException( rs, e );
}
}

} catch( RecordStoreException e ){
logger.logException( rs, e );
} finally {
logger.logEnd( rs );
}
}

// A logger that outputs to a PrintStream.

public static class PrintStreamLogger implements Logger {
public static final int COLS_MIN = 10;
public static final int COLS_DEFAULT = 20;

private int cols;
private int numBytes;
private StringBuffer hBuf;
private StringBuffer cBuf;
private StringBuffer pBuf;
private PrintStream out;

public PrintStreamLogger( PrintStream out ){
this( out, COLS_DEFAULT );
}

public PrintStreamLogger( PrintStream out, int cols ){
this.out = out;
this.cols = ( cols > COLS_MIN ? cols : COLS_MIN );
}

private char convertChar( char ch ){
if( ch < 0x20 ) return '.';
return ch;
}

public void logEnd( RecordStore rs ){
out.println( "\nActual size of records = "
+ numBytes );
printChar( '-', cols * 4 + 1 );

hBuf = null;
cBuf = null;
pBuf = null;
}

public void logException( String name, Throwable e ){
out.println( "Exception while analyzing " +
name + ": " + e );
}

public void logException( RecordStore rs, Throwable e ){
String name;

try {
name = rs.getName();
} catch( RecordStoreException rse ){
name = "";
}

logException( name, e );
}

public void logRecord( RecordStore rs, int id,
byte[] data, int len ){
if( len < 0 && data != null ){
len = data.length;
}

hBuf.setLength( 0 );
cBuf.setLength( 0 );

numBytes += len;

out.println( "Record #" + id + " of length "
+ len + " bytes" );

for( int i = 0; i < len; ++i ){
int b = Math.abs( data[i] );
String hStr = Integer.toHexString( b );

if( b < 0x10 ){
hBuf.append( '0');
}

hBuf.append( hStr );
hBuf.append( ' ' );

cBuf.append( convertChar( (char) b ) );

if( cBuf.length() == cols ){
out.println( hBuf + " " + cBuf );

hBuf.setLength( 0 );
cBuf.setLength( 0 );
}
}

len = cBuf.length();

if( len > 0 ){
while( len++ < cols ){
hBuf.append( " " );
cBuf.append( ' ' );
}

out.println( hBuf + " " + cBuf );
}
}

public void logStart( RecordStore rs ){
hBuf = new StringBuffer( cols * 3 );
cBuf = new StringBuffer( cols );
pBuf = new StringBuffer();

printChar( '=', cols * 4 + 1 );

numBytes = 0;

try {
out.println( "Record store: "
+ rs.getName() );
out.println( " Number of records = "
+ rs.getNumRecords() );
out.println( " Total size = "
+ rs.getSize() );
out.println( " Version = "
+ rs.getVersion() );
out.println( " Last modified = "
+ rs.getLastModified() );
out.println( " Size available = "
+ rs.getSizeAvailable() );
out.println( "" );
} catch( RecordStoreException e ){
logException( rs, e );
}
}

private void printChar( char ch, int num ){
pBuf.setLength( 0 );
while( num-- > 0 ){
pBuf.append( ch );
}
out.println( pBuf.toString() );
}
}

// A logger that outputs to System.out.

public static class SystemLogger
extends PrintStreamLogger {
public SystemLogger(){
super( System.out );
}

public SystemLogger( int cols ){
super( System.out, cols );
}
}