Java Modified UTF-8

Unicode

Unicode,官方中文名称为统一码,是计算机科学领域字符集的业界标准。它整理、编码了世界上大部分的文字系统,使得电脑可以用更为简单的方式来呈现和处理文字。

简单来说就是某一个文字或者符号,对应(映射)着一个整数值,范围为0x0000 ~ 0x1FFFF

Unicode划分将字符集划分为17组,其中第一组为基本字符集(也叫基本多文种平面Basic Multilingual Plane)范围为0x0000 ~ 0xFFFF,包含了大部分国家常用的字符串和符号,其中0xD800 ~ 0xDFFF不映射字符,所以基本平面能够表示的字符个数为 (65536 - 2048)

其余的字符集为增补字符集(也叫多文种补充平面Supplementary Multilingual Plane),范围为0x10000 ~ 0x1FFFFF

所以Unicode的字符集个数为 2^17 - 2048 = 1112064

UTF-16

UTF-16是对Unicode的字符集的编码,Java的Char类型占用两个字节,用的就是UTF-16进行编码。对于基本字符集使用一个char就能够表示,比如:

‘A’ 对于的二进制为: 0x00 0x41

对于增补字符集,一个char表示不了,需要用两个char,但是对于每一个char的解析,我们需要知道它是落在了基本字符集里还是增补字符集里面,所以就需要一个类似转义的操作,由于基本字符集里的0xD800 ~ 0xDFFF是不映射字符的,可以用来转义。计算过程如下:

扩展平面减去基本平面数量 0x10000 ~ 0x10FFFF - 0x10000 = 0x00000 ~ 0xFFFFF, 共20位,分别对前10位和后10位进行编码.

0xD800 + 前10位 前两个字节,又叫高代理对(High Surrogate), 范围为0xD800 ~ 0xD8FF(0xD800 + 0x3FF)

0xDC00 + 后10位 后两个子集,又叫低代理对(Low Surrogate),范围为0xDC00 ~ 0xDFFF(0xDC00 + 0x3FF)

例如’𠎠’字符,对应的Unicode码为 0x203A0,为增补字符集里面的字符,所以需要使用代理对进行编码:

首先减去基本字符集的范围:
0x203A0 - 0x10000 = 0x103A0, 对应的二进制位0001 0000 0011 1010 0000

对前十位进行编码:
0xD800 + 0100 0000 = 0xD800 + 0x40 = 0xD840

对后十位进行编码
0xDC00 + 11 1010 0000 = 0xDC00 + 0x3A0 = 0xDFA0

所以编码后两个字符内容为:\uD840\uDFA0

Regular UTF-8

UTF-8 同样是对Unicode字符集的编码,不过他使用变长的规则进行编码,对于大部分基本字符集里面的汉字字符,需要3个字节进行编码,但是对于英文字符ASCII,只需要一个字节。

编码规则如下:

对于与范围为0x00 ~ 0x7F的Uicode码: 0XXXXXXX

对于超过0x7F的字符,使用变长的规则,低位均为10开头,从第二位位后面到0之间1的个数就是后面字节的个数

0x7F ~ 0x7FF 110XXXXX 10XXXXXX

0x800 ~ 0xFFFF 1110XXXX 10XXXXXX 10XXXXXX

0x10000 ~ 0x10FFFF 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX

例如字符’A’ 对于的编码为 0x41

字符’𠎠’对于的编码:

0x203A0落在区间0x10000 ~ 0x10FFFF,对于的二进制为0010 0000 0011 1010 0000,将位填入上面的占位符(X)得到:

1111 0000 1010 0000 1000 1110 1010 0000 即 0xF0 0xA0 0x8E 0xA0

Modified UTF-8

Modified UTF-8对UTF-8做了一些修改,起源于Java,java.io.DataInput里有对其描述(https://developer.android.com/reference/java/io/DataInput),区别主要有:

继续使用增补字符集的字符’𠎠’示例:

从上面可以知道UTF-16的高代理对于低代理对字节为: \uD840 \uDFA0

分别对0xD840与0xDFA0进行UTF-8编码

0xD840 对于二进制 1101 1000 0100 0000

0xDFA0 对于二进制 1101 1111 1010 0000

对高代理对进行编码 得到 11101101 10100001 10000000 即 0xED 0xA1 0x80

对低代理对进行编码得到 11101101 10111110 10100000 即 0xED 0xBE 0xA0

所以最终得到的编码内容为 0xED 0xA1 0xBD 0xED 0xB8 0x88

Modified UTF-8主要用在了两个地方:

GetStringUTFChars

const char * GetStringUTFChars(JNIEnv *env, jstring string,
jboolean *isCopy);

Returns a pointer to an array of bytes representing the string in modified UTF-8 encoding

但是对于增补字符集,在Android R上测试了这个函数,’𠎠’字符得到的是用Unicode进行UTF8编码的,并不是使用代理对处理的Modified UTF-8:

11100000 10100000 10001110 10100000 即 0xF0 0xA0 0x8E 0xA0

查了下,很早的版本就有人反馈这个现象 https://github.com/android/ndk/issues/283

但是文档或者实现都没有改,所以涉及到JNI使用UTF-8处理增补字符集的时候,需要留意下有没有问题。