本文主要讨论Unicode的编码与各种实现,着重讨论UTF-16,UTF-8的实现规则,以及Big-endian和Little-Endian的存储规则。


一、Unicode编码

    Unicode出现之前已经有各种编码标准:ANSI、ISO8859-1、GB2312、GBK以及BIG-5等。Unicode试图统一各种编码,在Unicode演进过程中,也有自身不断修复的过程:刚开始的时候认为16位可以表达65535个字符,已经足够收集所有的字符;后来随着大量中文、韩文和日文等表意文字的加入,已经超出了65535个字符,16位已经不能描述所有的字符集了。

    在Unicode字符集中的某个字符对应的代码值,称作代码点(Code Point),用16进制书写,并加上U+前缀。比如,‘田’的代码点是U+7530;‘A’的代码点是U+0041。

    前面说过,Unicode的字符已经超过16位所能表达的范围,把所有这些CodePoint分成17个代码平面(Code Plane)

  • U+0000 ~ U+FFFF划入基本多语言平面(Basic MultilingualPlane,简记为BMP);
  • 其余划入16个辅助平面(Supplementary Plane),代码点范围U+10000 ~ U+10FFFF

    虽然这样划分,但并不是每个Plane中的Codepoint都对应有字符,这里面有保留的,还有特殊用途的。


二、Unicode编码的实现


    Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的,但是在实际存储和传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(UnicodeTransformation Format,简称为UTF)。

    对Unicode编码的实现方式有UTF-16BE、UTF-16LE、UTF-8、UTF-7以及UTF-32等实现方式,目前通用的实现方式是UTF-16LE、UTF-16BE和UTF-8。


2.1 UTF-16

    UTF-16是用16bit编码来表达Unicode,这样表达范围是216(即65536)。如果表达BMP内的字符,用一个UTF-16就可表达,对于辅助平面内的字符,UTF-16有巧妙的设计。

    BMP内,从U+D800U+DFFF之间的码位区段是永久保留不映射到字符, UTF-16利用保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。


对U+0000 ~ U+D7FF以及U+E000 ~ U+FFFF的编码

    UTF-16与UCS-2编码这个范围内的码位为单个16比特长的码元,数值等价于对应的码位。BMP中的这些码位是仅有的码位可以在UCS-2被表示。

对U+10000 ~ U+10FFFF的编码

    辅助平面(Supplementary Planes)中的码位,在UTF-16中被编码为一对16比特长的码元(即32bit,4Bytes),称作代理对(surrogatepair)。


 具体方法是:

UTF-16解码

hi \ lo

DC00

DC01

   …   

DFFF

D800

10000

10001

103FF

D801

10400

10401

107FF

  

DBFF

10FC00

10FC01

10FFFF

  1. 码位减去0x10000, 得到的值是长度为20bit(0..0xFFFFF);
  2. 步骤1得到数值的高位的10比特的值(值范围为0..0x3FF)被加上0xD800得到第一个码元或称作高位代理(high surrogate)或前导代理(lead surrogate)。值的范围是0xD800..0xDBFF
  3. 步骤1得到数值的低位的10比特的值(值范围为0..0x3FF)被加上0xDC00得到第二个码元或称作低位代理(low surrogate)或后尾代理(trail surrogate)。值的范围是0xDC00..0xDFFF

    这样,这个范围内的字符就被编码成了一个代理对[lead surrogate,trail surrogate]:两个16bits码元,取值范围分别是0xD800..0xDBFF,0xDC00..0xDFF。而BMP中得到的码元范围0x0000..0xFFFF中,0xD800..0xDFFF又是保留的,所以这三个区段是相互不重叠的,在解码时很容易实现。UTF-16解码高位代理+低位代理得到的码元与码位的对应关系如上表所示:

    下面以对U+64321的UTF-16编码为例,看一下对于辅助平面内的字符的编码实现:

V  = 0x64321

Vx = V - 0x10000

   = 0x54321

   = 01010100 0011 0010 0001

 

Vh = 01 0101 0000 // Vx 的高位部份的 10 bits

Vl = 11 0010 0001 // Vx 的低位部份的 10 bits

w1 = 0xD800 //结果的前16位元初始值

w2 = 0xDC00 //结果的后16位元初始值

 

w1 = w1 | Vh

   = 1101 1000 0000 0000

   |       01 0101 0000

   = 1101 1001 0101 0000

   = 0xD950

 

w2 = w2 | Vl

   = 1101 1100 0000 0000

   |       11 0010 0001

   = 1101 1111 0010 0001

   = 0xDF21

    所以这个字 U+64321 最后的 UTF-16 编码是:

0xD950 0xDF21


对于生成的编码,因为对于16Bits的两字节,还有存取先后的问题,还有Endian的问题,这在后续讲述。


2.2 UTF-8

    UTF-8(8-bitUnicode Transformation Format)是一种针对Unicode的可变长度字符编码,使用一至四个字节为每个字符编码:

  •  Unicode范围为U+0000..U+007F 的128个ASCII字符只需一个字节编码;
  •  Unicode范围为U+0080..U+07FF的字符需要二个字节编码;
  •  Unicode范围为U+0800..U+FFFF的其他BMP中的字符(这包含了大部分常用字)使用三个字节编码;
  •  Unicode 辅助平面的字符(其他极少使用的字符)使用四字节编码。

    对上述提及的第四种字符而言,UTF-8使用四个字节来编码似乎太耗费资源了。但UTF-8对所有常用的字符都只用三个字节表达,而且UTF-16编码对前述的第四种字符同样需要四个字节来编码,而如果是ASCII居多的字符,UTF-8能极大的节约存储空间。UTF-8逐渐成为电子邮件、网页及其他储存或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。互联网邮件联盟(IMC)建议所有电子邮件软件都支持UTF-8编码。


    对CodePoint各个范围内的字符进行UTF-8编码的规则如下:

Code point

UTF-8字节流

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+D800到U+DFFF之间的区段在Unicode的定义中没有具体字符使用的,被用来在UTF-16编码中对辅助平面的字符进行编码。


    下面以“田”(CodePoint为U+7530)为例,看如何对其进行UTF-8编码:

  •  U+7530落在U+0800..U+FFFF区间,采用三字节编码;
  •  0x7530转换为二进制为111 010100 110000;
  •  代入表中,得到111001111001010010110000

    这样,得到“田”(U+7530)的UTF-8编码:0xE7 94 B0。


    知道UTF-8的编码规则,我们可以对于UTF-8编码中的任意字节B,进行下面解码:

  •  如果B的第一位为0,则B为ASCII码,并且B独立的表示一个字符;如果B的第一位为1,第二位为0,则B为一个非ASCII字符(该字符由多个字节表示)中的一个字节,并且不为字符的第一个字节编码(字符的第一个字节之外的后编码);
  •  如果B的前两位为1,第三位为0,则B为一个非ASCII字符(该字符由多个字节表示)中的第一个字节,并且该字符由两个字节表示;
  •  如果B的前三位为1,第四位为0,则B为一个非ASCII字符(该字符由多个字节表示)中的第一个字节,并且该字符由三个字节表示;
  •  如果B的前四位为1,第五位为0,则B为一个非ASCII字符(该字符由多个字节表示)中的第一个字节,并且该字符由四个字节表示。

 

2.3 UCS-2 vs UTF-16,UCS-4 vs UTF-32

    UCS-2每个字符占用2个字节。UCS-2是UTF-16的子集。在没有辅助平面前,UTF-16与UCS-2所指的是同一的意思。但当引入辅助平面字符后,UTF-16加入了对辅助平面内的字符的支持。现在若有软件声称自己支持UCS-2编码,那其实是暗指它不支持UTF-16中超过2bytes的字集。亦即,对于小于0x10000的UCS码,UTF-16编码就等于UCS码。Java早期版本对Unicode的支持,就只是UCS-2的支持,现在加入了对UTF-16的完整支持。

 

    UCS-4UTF-32的意义一致,对每个字符都使用4字节(31位字符集,加上恒为0的首位,共需占据32位)。理论上最多能表示231个字符,完全可以涵盖一切语言所用的符号。虽然每一个码位使用固定长定的字节看似方便,对于普通只需要2个字节存储的常用字占绝大对数的字符集来说,却极大的浪费了空间,并没怎么得到应用。


三、Big-Endian/Little-Endian与BOM

    在讲UTF-16编码方式时说到,最终生成的编码可能是2个字节(BMP内的字符),这两个字节在传输和存储过程中,高/低位位置不同,是不同的字符。比如,“田”的UTF-16编码是0x7530,但是如果存成0x3075,就变成了“ふ”,另外的字符。

    所以,为了识别一个编码过的字符的存储顺序,必须用特殊字符来指示。Unicode字符中U+FEFF被用来指示这种存储顺序,被称作Byte Order Mark(BOM)。

  •  Big-Endian:最低位地址存放高位字节,可称高位优先,内存从最低地址开始按顺序存放(高数位数字先写)。最高位字节放最前面。
  •  Little Endian:最低位地址存放低位字节,可称低位优先,内存从最低地址开始按顺序存放(低数位数字先写)。最低位字节放最前面。

    所以,BOM在Big-Endian系统上存储为FE FF;而在Big-Endian系统上存储则为FF FE。在以Big-Endian存储的UTF-16(UTF-16BE)的文件的开头,用FEFF指示;以Little-Endian存储的UTF-16(UTF-16LE)的文件的开头,用FEFF指示。

    在Windows的记事本上,另存为的时候,你可以选择不同的Endian存储,然后再用纯文本编辑工具(Ultra-edit)来检验一下,UTF-16的存储顺序。

    BOM的UTF-8编码为11101111 1011101110111111 (EF BB BF),所以一般EF BB BF被放在文本的开头,用来指示其编码为UTF-8。


【附】基本概念对照

Code Point码位

Code Unit码元是指一个已编码的文本中具有最短的比特组合的单元。对于UTF-8来说,码元是8比特长;对于UTF-16来说,码元是16比特长;对于UTF-32来说,码元是32比特长。

BMP - Basic Multilingual Plane

UTF - Unicode Transformation Format

BOM – Byte Order Mark

UCS - Universal Character Set