本文介紹JavaME中文編碼的相關問題,這個問題一度是互聯網上的開發者們討論的熱門話題。本文整理和綜合了網上眾多相關內容,盡可能的為開發者提供一個全面、系統的認識。
2.3 UTF-8
UTF的全稱是UCS Transformation Format,即把Unicode轉做某種格式的意思。目前存在的UTF格式有:UTF-7, UTF-7.5, UTF-8, UTF-16, 以及 UTF-32,本文討論UTF-8格式。
UTF-8是UNICODE的一種變長字符編碼,理論上使用1~6個字節來編碼UNICODE。
雖然理論上UTF-8最多為6個字節,但是,由于雙字節的Unicode最大為0XFFFF,所以雙字節的Unicode轉為UTF-8后最長為3個字節。
下列字節串用來表示一個字符。 用到哪個串取決于該字符在 Unicode 中的序號。
U-00000000 - U-0000007F: 0xxxxxxx
U-00000080 - U-000007FF: 110xxxxx 10xxxxxx
U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
上表中的xxx為Unicode編碼的二進制數據。例如:“中”字,Unicode編碼為4E2D。
Unicode: 4E 2D 01001110 00101101
UTF-8: E4 B8 AD 11100100 10111000 10101101
對于英文來說,UTF-8跟ISO8859-1一樣節約;但顯然中文等字符將為UTF-8付出更多。
3.1 編解碼方法
說到JavaME中的字符編碼問題,自然要從String類入手,在String類中我們可以找到字符編解碼的相關方法:
1. 解碼:
public String(byte[] bytes, String enc) throws UnsupportedEncodingException
2. 編碼:
public byte[] getBytes(String enc) throws UnsupportedEncodingException
舉例來說,“諾基亞”三個漢字的GB2312編碼為C5 B5 BB F9 D1 C7。
代碼段一,解碼試驗:
byte[] codes = {0XC5, 0XB5, 0XBB, 0XF9, 0XD1, 0XC7};
String string = new String(codes, “gb2312”);
testForm.append(string);
得到的結果為:諾基亞
代碼段二,編碼試驗:
byte[] codes = “諾基亞”.getBytes(“gb2312”);
for (int i = 0, t = codes.length; i < t; i ++) {
String hexByte = Integer.getHexString(codes[i]);
if (hexByte.length() > 2) {
hexByte = hexByte.subString(hexByte.length() - 2);
}
testForm.append(“0X”+ hexByte.subString + “, ”);
}
得到的結果為:0XC5, 0XB5, 0XBB, 0XF9, 0XD1, 0XC7,
3.2 檢查設備的編碼支持情況
一個最直接的獲取編碼支持的方法是使用System.getProperty(“microedition.encoding”),可以得到設備的默認的字符編碼,以NOKIA設備為例,得到的屬性值為ISO8859-1。
然而,通過這個方法的意義并不大。首先,它只能獲取到一個編碼格式,而一般設備都會支持很多種編碼規范;其次,這個屬性的數值與虛擬機的實現有很大關系,同樣以NOKIA S40v2為例,不論設備的目標市場使用什么語言,這個屬性統一為ISO8859-1[參考資料5],顯然ISO8859-1對于中文來說是毫無意義的。
再回過頭來看看上一節中的兩個方法,它們都會拋出一個UnsupportedEncodingException。利用這一點,我們可以自己來做一個設備支持編碼規范情況的測試。
boolean isEncodingSupported (String encoding) {
try {
"諾基亞".getBytes(encoding);
return true;
} catch (UnsupportedEncodingException uee) {
return false;
}
}
這里需要提醒的是,對于同一個編碼格式來說,可能會有很多種不同的名稱,例如Unicode在NOKIA的設備上用的是ucs-2,再例如utf-8來說,utf-8、utf8和utf_8都會有可能。對于這一點,CLDC的規范中并沒有給出嚴格的定義。所以在實際測試的過程中需要充分考慮到這個情況。
4.1 RMS存儲的中文問題
這個問題完全可以使用readUTF和writeUTF來解決。
用UTF8編碼向RecordStore中寫入中文:
String content = "中文字符";
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeUTF(content);
byte[] bytes = bos.toByteArray();
rs.addRecord(bytes, 0, bytes.length);
從RecordStore中讀出UTF8編碼的中文:
byte utfBytes[] = rs.getRecord(dbid);
DataInputStream dis=new DataInputStream(new ByteArrayInputStream(utfBytes));
String content = dis.readUTF();
當然,使用UTF8對于中文來說意味著更大的存儲空間,所以也可以使用類似Unicode和GB2312等2字節的編碼。在此之前,請通過3.2中描述的方式檢測編碼是否被支持。
用GB2312編碼向RecordStore中寫入中文:
String content = "中文字符";
byte[] bytes = content.getBytes("gb2312");
rs.addRecord(bytes, 0, bytes.length);
從RecordStore中讀出GB2312編碼的中文:
byte gb2312Bytes[] = rs.getRecord(dbid);
String content = new String(gb2312Bytes, "gb2312");
4.2 從Resource文件中讀取中文
大概可以分為兩種情況,一是這個文件遵循某種特定的格式,例如RPG游戲的關卡文件,其中包含地圖、事件和對話等數據,文件由特定的程序生成,為自有格式。這種情況基本上可以使用與上一小節中同樣的方式來解決。關鍵是,存儲和讀取的過程遵從同樣的數據和編碼格式。
另一種情況是txt文件,可能通過一些文本編輯工具生成。Txt文件中常見的編碼格式有Unicode、UTF8、Unicode Big Endian等。在我們讀取txt文件之前,最先要確認的就是這個txt文件所使用的編碼格式。
以UTF8為例:
in = getClass().getResourceAsStream(filename);
in.read(word_utf);
in.close();
string =new String(word_utf,"UTF-8");
對于Unicode來說,這里引用了參考1中的一段代碼,這段代碼實際上是在處理“低位在前”的Unicode:
public static String unicodeBytesToString(byte abyte0[], int i)
{
StringBuffer stringbuffer = new StringBuffer("");
for(int j = 0; j < i; )
{
int k = abyte0[j++]; //注意在這個地方進行了碼制的轉換
if(k < 0)
k += 256;
int l = abyte0[j++];
if(l < 0)
l += 256;
char c = (char)(k + (l << 8));//把高位和低位數組裝起來
stringbuffer.append(c);
}
}
對于高位在前的Unicode,可以使用和UTF8類似的方式。請注意,這里的"ucs-2"是針對諾基亞設備的,其他廠商設備可能與此不同,請查閱相關文檔或自行測試。
in = getClass().getResourceAsStream(filename);
in.read(word_ unicode);
in.close();
string =new String(word_unicode, "ucs-2");
實際上經過我在NOKIA設備上的測試,對于低位在前的Unicode,也可以使用這個方式。前提是需要確保在數據的最前端添加低位在前的BOM(0XFFFE)。
繼續使用“諾基亞”為例,高位在前和低位在前的的Unicode編碼分別為:
nokiaBE = {(byte)0x8b, (byte)0xfa, (byte)0x57, (byte)0xfa, (byte)0x4e, (byte)0x9a,}
nokiaLE = {(byte)0xfa, (byte)0x8b, (byte)0xfa, (byte)0x57, (byte)0x9a, (byte)0x4e,};
new String(nokiaBE, "ucs-2")的結果是“諾基亞”,而new String(nokiaLE, "ucs-2")的結果則是亂碼。然后,我們對nokiaLE做出修改:
nokiaLE = {(byte)0xff, (byte)0xfe,
(byte)0xfa, (byte)0x8b, (byte)0xfa, (byte)0x57, (byte)0x9a, (byte)0x4e,};
修改后,再次執行new String(nokiaLE, "ucs-2",則得到的結果也是“諾基亞”。
BTW,對于Resouce文件來說,雖然使用Unicode編碼存儲中文看起來像是比UTF-8要更節約,但是當Resource資源被打成Jar包時,壓縮后的文件大小可能很接近。
4.3通過網絡讀取中文
和前面描述的一樣,避免亂碼的關鍵同樣是保證編解碼使用同樣的格式,也就是客戶端與服務器段保持同樣的字符編碼。
對于socket連接來說,傳輸的內容可以使用自定義的數據格式,所以處理的方式完全和前面兩節是相同的,甚至可以向http學習,在數據的開頭約定字符編碼。
對于http連接,在http數據頭中已經約定了編碼格式,使用這個編碼格式解碼即可。另外一個很常見的問題,就是從xml文件中解析中文。對于這一點,在kxml2中已經有了很好的解決方案。
org.kxml2.io.KXmlParser.setInput(InputStream is, String _enc)
你可以通過_enc指定一個編碼格式,如果_enc為null,則Parser會根據數據的特性自動嘗試各種編碼格式。由于kxml為開源項目,如果這里的處理方式需要調整,你也可以自己動手去完善它的功能。
在kxml2中也增加了對wml文件的解析,有興趣的可以研究一下,這一部分我沒有作過嘗試。