bug 描述 :cordova项目 ,名字字段中使用了输入法中自带的表情文件,在网站上一切正常,同步到sqlite数据库的时候会显示乱码。同时一旦对该条数据进行查询操作,就会导致程序奔溃。
Modified UTF-8
所谓的MUTF-8编码,其实是对UTF-16字符编码的再编码。
如果你的Android App里用了C++代码去处理一些字符, 可能会遇到崩溃input is not valid Modified UTF-8
. 这个崩溃的原因在于编码不一致. 外部往往使用的是标准UTF-8编码, JNI里相关方法处理的是变种UTF-8编码, 虽说他俩名字上都有UTF-8, 但是本质上还是两种编码方式, 尽管他们有一部分是兼容的.
JNI里, 凡是和String相关的方法, 名字带有UTF的, 基本都是使用的MUTF-8(变种UTF-8, Modified UTF-8).
比如jstring NewStringUTF(JNIEnv *env, const char *bytes);
是使用MUTF-8的字节流构造jstring.const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
获取jstring以MUTF-8编码的字节数组.
但是Java里, 凡是和UTF-8相关的字符串操作, 都是使用标准UTF-8.String.getBytes("UTF-8")
拿到的字符串的标准UTF-8编码的字节数组new String(bytes, "UTF-8")
是使用标准UTF-8的字节流构造String.
许多研发并不知道UTF-8在Java层和JNI层的差异, 所以一旦遇到这种不同编码方式导致的问题, 会很懵逼.
在Android的官方Dex文件格式的文档中,对MUTF-8编码有如下描述,总结的很到位:
1)MUTF-8使用1到3个字节对UTF-16字符进行编码;
2)对于数值为0的情况,使用两个字节对其进行编码(编码后的值为0xC0和0x80);
3)采用类似于C语言中的空字符串(NULL,单字节数值为0)作为字符串结尾的标志;
4)对于UTF-16码点范围在U+10000到U+10FFFF的情况(补充字符),数值对中的每一个数值采用3字节对其编码。也就是说,对于这种情况,表示一个字符总共需要使用6个字节。
前面三点很好理解,对于第四点,理解起来有点困难,这里特别说明一下。
大家知道UTF-16使用16位来对字符进行编码,那么其取值范围就应该是0x0到0xFFFF,这已经可以表示很多字符了。但是,世界太大了,要表示的字符太多了,最终发现16位不够用了。那怎么办呢,只能继续扩展,将取值范围又向上扩展,从0x10000到0x10FFFF,称作扩展字符。这些扩展字符的值,显然不能再用16位来表示了,那就用两个16位值来表示。对于这种表示一个扩展字符使用两个16位数值的情况,UTF-16称作代替数值对(Surrogate Pair),其编码规则如下:
1)先将UTF-16补充码的数值减去0x10000;
2)将减掉之后的数值分为两个10比特的数值,假设高10位的值表示为Vh,低10位的值表示为Vl;
3)对于数值对中第一个16位的双字节来说,用0xD800加上高10位的值Vh;
4)对于数值对中第二个16位的双字节来说,用0xDC00加上低10位的值Vl。
MUTF-8和标准UTF-8的区别有两点.
一是空字符("\0")在MUTF-8里被编码为两个字节: 0xC080(即1100000010000000). 它在标准UTF-8里编码为0x00.
二是SP内的字符, 首先以UTF-16的编码方式编为一个前导代理和一个后尾代理, 然后再用标准UTF-8分别编码这两个代理.
BMP内的字符, 除了\0
, 其他的MUTF-8和UTF-8编码方式相同.
由于SP范围是U+10000 - U+10FFFF, 标准UTF-8将SP内的码位编码为4字节, 而代理区U+D800 - U+DFFF中的码位在标准UTF-8中被编为3字节. 因此MUTF-8对SP内的码位进行编码需要3+3 = 6字节, 比标准UTF-8多2字节. dex里的字符串以MUTF-8编码.
在调用NewStringUTF()时, dalvik虚拟机调用checkUtfString() 中的checkUtfBytes()对字符串的格式进行了校验.
the key point is "Modified UTF-8" is not like "Regular UTF-8", a legal Rgular UTF8 code sequence may be considered illegal against Modified UTF8.
One work around in NDK level is : converting utf8 to native utf16, then use NewString() instead of NewStringUTF().