1. 简介
本文介绍JavaME中文编码的相关问题,这个问题一度是互联网上的开发者们讨论的热门话题。本文整理和综合了网上众多相关内容,尽可能的为开发者提供一个全面、系统的认识。
文中的代码仅用来说明原理,可能很不完整,缺乏变量定义或者返回值,请谅解。部分代码直接来源于网上的其他资料。
感谢众多开发者在中文编码问题上做出的努力与探索。总结中有什么问题的话,欢迎大家指正:)
2. 术语介绍
2.1 ASCII
基于罗马字母表的一套电子计算器编码系统,是单字节的编码方式,每个ASCII字符占用1个字节(8bits),所以ASCII编码最多可以表示256个字符。它是美国信息交换标准委员会(American Standards Committee for Information Interchange)的缩写, 为美国英语通信所设计。
显然ASCII编码用来表示英文字母和字符是足够了的,但是对于中文和日文等众多的文字来说,是远远不够的。
跟ASCII类似的编码还有ISO8859-1。
2.2 UNICODE
双字节编码方式,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本基准转换、处理的要求。UNICODE支持欧洲、非洲、中东、亚洲(包括统一标准的东亚像形汉字和韩国像形文字)的文字。
UNICODE又可以分为“高位在前”和“低位在前”的两种格式,这和CPU的处理方式有点关系。这一点可以通过BOM(Byte Order Mark)来标示,若采用 “低位在前”方式编码,BOM 会表示为 0xFF 0xFE,而在 Unicode 的定义中是不存在 U+FFFE 这个字符的。若采用高位在前方式编码,BOM 会表示为 0xFE 0xFF,而 U+FEFF 刚好是在 Unicode 中的有效字符,代表的是一个不占空间的 space 符号,所以即使没被解释为 BOM,也不会对阅览者产生错误的信息。
但UINICODE也带来一些问题,当美国人看见自己每天最常用的字符需要用两倍的空间来保存时,自然会觉得这是一种浪费,他们一定会说:看看那堆0。于是新的编码方式又诞生了。
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付出更多。
2.4 GB2312
GB2312又称国标码,由国家标准总局发布,1981年5月1日实施,通行于大陆。新加坡等地也使用此编码。它是一个简化字的编码规范,当然也包括其他的符号、字母、日文假名等,共7445个图形字符,其中汉字占6763个。我们平时说6768个汉字,实际上里边有5个编码为空白,所以总共有6763个汉字。
GB2312规定“对任意一个图形字符都采用两个字节表示,每个字节均采用七位编码表示”,习惯上称第一个字节为“高字节”,第二个字节为“低字节”。GB2312中汉字的编码范围为,第一字节0xB0-0xF7(对应十进制为176-247),第二个字节0xA0-0xFE(对应十进制为160-254)。
GB2312具有很多扩展,其中GBK是微软对GB2312的扩展,GB18030则是2000年发布的国家标准,是到目前为止最新的国标汉字编码。
3.JavaME中的字符编解码
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的规范中并没有给出严格的定义。所以在实际测试的过程中需要充分考虑到这个情况。
3.3 readUTF() 和 writeUTF(String)
CLDC中还有两个方法跟字符编码有关系:DataInputStream中的readUTF()和DataOutputStream中的writeUTF(String)。根据两个方法的Java Doc,writeUTF(String)首先会向输出流中写入字符串编码成UTF8格式后的byte数组长度(2个字节),然后再将这个UTF8的byte数组写入。而readUTF()则是先从输入流中读取2个字节,组成一个short数值,在从输入流中读出这个数值长度的byte数据,再将这个byte数组解码成字符串。详细说明请参考DataInputStream和DataOutputStream的Java Document。
4.具体问题的解决
上一部分中介绍了CLDC平台上所有跟字符编解码相关的API。了解了这些内容以后,就可以结合具体的情况来考虑如何解决中文编码的问题了。
首先,中文编码问题中最常见的情况就是乱码,那么乱码是如何产生的呢?无非是以下几种情况:
- 指定的编码格式与数据实际的编码格式不符,造成数据被编码成指定的替代符号或被解释成与源字符完全不同的字符;
- 指定的编码格式正确,数据不完整或被篡改,造成数据无法在字符集中找到与其对应的编码;
所以,解决中文编码问题的几个基本思路是:
- 存储或传输前,确保数据被正确编码;
- 确保存储、读取和传输的过程完整、正确;
- 解码时,使用与编码时同样的编码格式;
- 确认编码格式是否被设备支持;
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文件的解析,有兴趣的可以研究一下,这一部分我没有作过尝试。
4.4 JAD中的中文
JAD文件如果需要使用中文,则需要使用UTF8格式。关于如何在JAD中写入UTF8数据,可以有很多方法,可以使用一些Ultral Editor之类的文本工具,或使用一些JAD/MENIFEST生成工具。你甚至可以自己编写一个JAD/MENIFEST的生成工具,对于JavaSE平台来说,这并不是一件太难的工作,你还可以给这个工具赋予更多的功能,例如自动填写vendor和version之类的字段。
特别说明,对于NOKIA Series 40v2等设备来说,通过OTA下载Jad/Jar时,需要在服务器端为JAD添加MIME type时标明encoding为UTF8,否则即使你的JAD确实使用了UTF8格式,安装之后仍然会是乱码。
4.5 Unicode和UTF8的互转
在网上有一些Unicode和UTF8互相转换的代码,是直接通过编码格式去实现的,从前文叙述的Unicode和UTF8之间的关系来看,Unicode和UTF8之间很容易互相转换,计算量也不会太大。这里就不在把这些代码贴出来了,有兴趣的可以自己找一下,或者自己写一个。
但是,实际上Unicode和UTF8这两个编码格式基本是所有设备都会支持的,直接通过String类的编解码方法互转就可以了。
4.6设备不支持的编码格式
如果需要使用设备不支持的编码格式,那么必然将付出一些额外的代价。例如,如果设备不支持GB2312,则你可以有如下的几个选项中选择其一:
- 在你的程序中包含一个GB2312的字符对应表;(也许GB2312你还可以接收,但是GB18030要怎么办呢?)
- 做一个代理服务器,将GB2312转解码成可以被设备支持的编码;
- 放弃GB2312,选择其他编码;