本文介紹JavaME中文編碼的相關(guān)問題,這個問題一度是互聯(lián)網(wǎng)上的開發(fā)者們討論的熱門話題。本文整理和綜合了網(wǎng)上眾多相關(guān)內(nèi)容,盡可能的為開發(fā)者提供一個全面、系統(tǒng)的認識。
文中的代碼僅用來說明原理,可能很不完整,缺乏變量定義或者返回值,請諒解。部分代碼直接來源于網(wǎng)上的其他資料。
感謝眾多開發(fā)者在中文編碼問題上做出的努力與探索。總結(jié)中有什么問題的話,歡迎大家指正:)
2. 術(shù)語介紹
2.1 ASCII
基于羅馬字母表的一套電子計算器編碼系統(tǒng),是單字節(jié)的編碼方式,每個ASCII字符占用1個字節(jié)(8bits),所以ASCII編碼最多可以表示256個字符。它是美國信息交換標準委員會(American Standards Committee for Information Interchange)的縮寫, 為美國英語通信所設(shè)計。
顯然ASCII編碼用來表示英文字母和字符是足夠了的,但是對于中文和日文等眾多的文字來說,是遠遠不夠的。
跟ASCII類似的編碼還有ISO8859-1。
2.2 UNICODE
雙字節(jié)編碼方式,它為每種語言中的每個字符設(shè)定了統(tǒng)一并且唯一的二進制編碼,以滿足跨語言、跨平臺進行文本基準轉(zhuǎn)換、處理的要求。UNICODE支持歐洲、非洲、中東、亞洲(包括統(tǒng)一標準的東亞像形漢字和韓國像形文字)的文字。
UNICODE又可以分為“高位在前”和“低位在前”的兩種格式,這和CPU的處理方式有點關(guān)系。這一點可以通過BOM(Byte Order Mark)來標示,若采用 “低位在前”方式編碼,BOM 會表示為 0xFF 0xFE,而在 Unicode 的定義中是不存在 U+FFFE 這個字符的。若采用高位在前方式編碼,BOM 會表示為 0xFE 0xFF,而 U+FEFF 剛好是在 Unicode 中的有效字符,代表的是一個不占空間的 space 符號,所以即使沒被解釋為 BOM,也不會對閱覽者產(chǎn)生錯誤的信息。
但UINICODE也帶來一些問題,當(dāng)美國人看見自己每天最常用的字符需要用兩倍的空間來保存時,自然會覺得這是一種浪費,他們一定會說:看看那堆0。于是新的編碼方式又誕生了。
2.3 UTF-8
UTF的全稱是UCS Transformation Format,即把Unicode轉(zhuǎn)做某種格式的意思。目前存在的UTF格式有:UTF-7, UTF-7.5, UTF-8, UTF-16, 以及 UTF-32,本文討論UTF-8格式。
UTF-8是UNICODE的一種變長字符編碼,理論上使用1~6個字節(jié)來編碼UNICODE。
雖然理論上UTF-8最多為6個字節(jié),但是,由于雙字節(jié)的Unicode最大為0XFFFF,所以雙字節(jié)的Unicode轉(zhuǎn)為UTF-8后最長為3個字節(jié)。
下列字節(jié)串用來表示一個字符。 用到哪個串取決于該字符在 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編碼的二進制數(shù)據(jù)。例如:“中”字,Unicode編碼為4E2D。
Unicode: 4E 2D 01001110 00101101 UTF-8: E4 B8 AD 11100100 10111000 10101101
對于英文來說,UTF-8跟ISO8859-1一樣節(jié)約;但顯然中文等字符將為UTF-8付出更多。
2.4 GB2312
GB2312又稱國標碼,由國家標準總局發(fā)布,1981年5月1日實施,通行于大陸。新加坡等地也使用此編碼。它是一個簡化字的編碼規(guī)范,當(dāng)然也包括其他的符號、字母、日文假名等,共7445個圖形字符,其中漢字占6763個。我們平時說6768個漢字,實際上里邊有5個編碼為空白,所以總共有6763個漢字。
GB2312規(guī)定“對任意一個圖形字符都采用兩個字節(jié)表示,每個字節(jié)均采用七位編碼表示”,習(xí)慣上稱第一個字節(jié)為“高字節(jié)”,第二個字節(jié)為“低字節(jié)”。GB2312中漢字的編碼范圍為,第一字節(jié)0xB0-0xF7(對應(yīng)十進制為176-247),第二個字節(jié)0xA0-0xFE(對應(yīng)十進制為160-254)。
GB2312具有很多擴展,其中GBK是微軟對GB2312的擴展,GB18030則是2000年發(fā)布的國家標準,是到目前為止最新的國標漢字編碼。
3.JavaME中的字符編解碼
3.1 編解碼方法
說到JavaME中的字符編碼問題,自然要從String類入手,在String類中我們可以找到字符編解碼的相關(guān)方法:
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);
得到的結(jié)果為:諾基亞
代碼段二,編碼試驗:
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 + “, ”); }
得到的結(jié)果為:0XC5, 0XB5, 0XBB, 0XF9, 0XD1, 0XC7,
3.2 檢查設(shè)備的編碼支持情況
一個最直接的獲取編碼支持的方法是使用System.getProperty(“microedition.encoding”),可以得到設(shè)備的默認的字符編碼,以NOKIA設(shè)備為例,得到的屬性值為ISO8859-1。
然而,通過這個方法的意義并不大。首先,它只能獲取到一個編碼格式,而一般設(shè)備都會支持很多種編碼規(guī)范;其次,這個屬性的數(shù)值與虛擬機的實現(xiàn)有很大關(guān)系,同樣以NOKIA S40v2為例,不論設(shè)備的目標市場使用什么語言,這個屬性統(tǒng)一為ISO8859-1[參考資料5],顯然ISO8859-1對于中文來說是毫無意義的。
再回過頭來看看上一節(jié)中的兩個方法,它們都會拋出一個UnsupportedEncodingException。利用這一點,我們可以自己來做一個設(shè)備支持編碼規(guī)范情況的測試。
boolean isEncodingSupported (String encoding) { try { "諾基亞".getBytes(encoding); return true; } catch (UnsupportedEncodingException uee) { return false; } }
這里需要提醒的是,對于同一個編碼格式來說,可能會有很多種不同的名稱,例如Unicode在NOKIA的設(shè)備上用的是ucs-2,再例如utf-8來說,utf-8、utf8和utf_8都會有可能。對于這一點,CLDC的規(guī)范中并沒有給出嚴格的定義。所以在實際測試的過程中需要充分考慮到這個情況。
3.3 readUTF() 和 writeUTF(String)
CLDC中還有兩個方法跟字符編碼有關(guān)系:DataInputStream中的readUTF()和DataOutputStream中的writeUTF(String)。根據(jù)兩個方法的Java Doc,writeUTF(String)首先會向輸出流中寫入字符串編碼成UTF8格式后的byte數(shù)組長度(2個字節(jié)),然后再將這個UTF8的byte數(shù)組寫入。而readUTF()則是先從輸入流中讀取2個字節(jié),組成一個short數(shù)值,在從輸入流中讀出這個數(shù)值長度的byte數(shù)據(jù),再將這個byte數(shù)組解碼成字符串。詳細說明請參考DataInputStream和DataOutputStream的Java Document。
4.具體問題的解決
上一部分中介紹了CLDC平臺上所有跟字符編解碼相關(guān)的API。了解了這些內(nèi)容以后,就可以結(jié)合具體的情況來考慮如何解決中文編碼的問題了。
首先,中文編碼問題中最常見的情況就是亂碼,那么亂碼是如何產(chǎn)生的呢?無非是以下幾種情況:
- 指定的編碼格式與數(shù)據(jù)實際的編碼格式不符,造成數(shù)據(jù)被編碼成指定的替代符號或被解釋成與源字符完全不同的字符;
- 指定的編碼格式正確,數(shù)據(jù)不完整或被篡改,造成數(shù)據(jù)無法在字符集中找到與其對應(yīng)的編碼;
所以,解決中文編碼問題的幾個基本思路是:
- 存儲或傳輸前,確保數(shù)據(jù)被正確編碼;
- 確保存儲、讀取和傳輸?shù)倪^程完整、正確;
- 解碼時,使用與編碼時同樣的編碼格式;
- 確認編碼格式是否被設(shè)備支持;
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();
當(dāng)然,使用UTF8對于中文來說意味著更大的存儲空間,所以也可以使用類似Unicode和GB2312等2字節(jié)的編碼。在此之前,請通過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游戲的關(guān)卡文件,其中包含地圖、事件和對話等數(shù)據(jù),文件由特定的程序生成,為自有格式。這種情況基本上可以使用與上一小節(jié)中同樣的方式來解決。關(guān)鍵是,存儲和讀取的過程遵從同樣的數(shù)據(jù)和編碼格式。
另一種情況是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++]; //注意在這個地方進行了碼制的轉(zhuǎn)換 if(k < 0) k += 256; int l = abyte0[j++]; if(l < 0) l += 256; char c = (char)(k + (l << 8));//把高位和低位數(shù)組裝起來 stringbuffer.append(c); } }
對于高位在前的Unicode,可以使用和UTF8類似的方式。請注意,這里的"ucs-2"是針對諾基亞設(shè)備的,其他廠商設(shè)備可能與此不同,請查閱相關(guān)文檔或自行測試。
in = getClass().getResourceAsStream(filename); in.read(word_ unicode); in.close(); string =new String(word_unicode, "ucs-2");
實際上經(jīng)過我在NOKIA設(shè)備上的測試,對于低位在前的Unicode,也可以使用這個方式。前提是需要確保在數(shù)據(jù)的最前端添加低位在前的BOM(0XFFFE)。
繼續(xù)使用“諾基亞”為例,高位在前和低位在前的的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")的結(jié)果是“諾基亞”,而new String(nokiaLE, "ucs-2")的結(jié)果則是亂碼。然后,我們對nokiaLE做出修改:
nokiaLE = {(byte)0xff, (byte)0xfe, (byte)0xfa, (byte)0x8b, (byte)0xfa, (byte)0x57, (byte)0x9a, (byte)0x4e,};
修改后,再次執(zhí)行new String(nokiaLE, "ucs-2",則得到的結(jié)果也是“諾基亞”。
BTW,對于Resouce文件來說,雖然使用Unicode編碼存儲中文看起來像是比UTF-8要更節(jié)約,但是當(dāng)Resource資源被打成Jar包時,壓縮后的文件大小可能很接近。
4.3通過網(wǎng)絡(luò)讀取中文
和前面描述的一樣,避免亂碼的關(guān)鍵同樣是保證編解碼使用同樣的格式,也就是客戶端與服務(wù)器段保持同樣的字符編碼。
對于socket連接來說,傳輸?shù)膬?nèi)容可以使用自定義的數(shù)據(jù)格式,所以處理的方式完全和前面兩節(jié)是相同的,甚至可以向http學(xué)習(xí),在數(shù)據(jù)的開頭約定字符編碼。
對于http連接,在http數(shù)據(jù)頭中已經(jīng)約定了編碼格式,使用這個編碼格式解碼即可。另外一個很常見的問題,就是從xml文件中解析中文。對于這一點,在kxml2中已經(jīng)有了很好的解決方案。
org.kxml2.io.KXmlParser.setInput(InputStream is, String _enc)
你可以通過_enc指定一個編碼格式,如果_enc為null,則Parser會根據(jù)數(shù)據(jù)的特性自動嘗試各種編碼格式。由于kxml為開源項目,如果這里的處理方式需要調(diào)整,你也可以自己動手去完善它的功能。
在kxml2中也增加了對wml文件的解析,有興趣的可以研究一下,這一部分我沒有作過嘗試。
4.4 JAD中的中文
JAD文件如果需要使用中文,則需要使用UTF8格式。關(guān)于如何在JAD中寫入UTF8數(shù)據(jù),可以有很多方法,可以使用一些Ultral Editor之類的文本工具,或使用一些JAD/MENIFEST生成工具。你甚至可以自己編寫一個JAD/MENIFEST的生成工具,對于JavaSE平臺來說,這并不是一件太難的工作,你還可以給這個工具賦予更多的功能,例如自動填寫vendor和version之類的字段。
特別說明,對于NOKIA Series 40v2等設(shè)備來說,通過OTA下載Jad/Jar時,需要在服務(wù)器端為JAD添加MIME type時標明encoding為UTF8,否則即使你的JAD確實使用了UTF8格式,安裝之后仍然會是亂碼。
4.5 Unicode和UTF8的互轉(zhuǎn)
在網(wǎng)上有一些Unicode和UTF8互相轉(zhuǎn)換的代碼,是直接通過編碼格式去實現(xiàn)的,從前文敘述的Unicode和UTF8之間的關(guān)系來看,Unicode和UTF8之間很容易互相轉(zhuǎn)換,計算量也不會太大。這里就不在把這些代碼貼出來了,有興趣的可以自己找一下,或者自己寫一個。
但是,實際上Unicode和UTF8這兩個編碼格式基本是所有設(shè)備都會支持的,直接通過String類的編解碼方法互轉(zhuǎn)就可以了。
4.6設(shè)備不支持的編碼格式
如果需要使用設(shè)備不支持的編碼格式,那么必然將付出一些額外的代價。例如,如果設(shè)備不支持GB2312,則你可以有如下的幾個選項中選擇其一:
- 在你的程序中包含一個GB2312的字符對應(yīng)表;(也許GB2312你還可以接收,但是GB18030要怎么辦呢?)
- 做一個代理服務(wù)器,將GB2312轉(zhuǎn)解碼成可以被設(shè)備支持的編碼;
- 放棄GB2312,選擇其他編碼;