由Emoji表情发现的JNI GetStringUTFChars()隐藏的问题

我们App的消息收发底层由C++实现,自然就需要使用JNI,开始的方案是将消息内容String字符串直接向下传,然后在JNI中解析为C++ string形式,
当然我们使用的是GetStringUTFChars方法。然而消息发送后,发现Emoji表情在服务端无法正确解析。在java层和jni层分别加log后,我发现java层的消息内容的16进制字符串与JNI使用GetStringUTFChars方法得到C++格式string的16进制字符串内容并不一样,我想这应该就是产生问题的原因,当然想法需要实际的验证。
我更改了消息发送协议,在java层把消息内容由String改为byte[]数组形式,这样JNI层就不再需要使用GetStringUTFChars方法转换消息内容。再次测试,bingo,Emoji表情收发解析成功。
那不禁要问为什么会这样呢?GetStringUTFChars到底做了什么?
先看Java的String.getBytes()方法得到UTF-8编码byte[]的源码,

public byte[] getBytes() {
    return getBytes(Charset.defaultCharset());
}

public static Charset defaultCharset() {
    return DEFAULT_CHARSET;//就是UTF-8了
}

public byte[] getBytes(Charset charset) {
    String canonicalCharsetName = charset.name();
    if (canonicalCharsetName.equals("UTF-8")) {
        return CharsetUtils.toUtf8Bytes(this, 0, count);
    } else if (canonicalCharsetName.equals("ISO-8859-1")) {
        return CharsetUtils.toIsoLatin1Bytes(this, 0, count);
    } else if (canonicalCharsetName.equals("US-ASCII")) {
        return CharsetUtils.toAsciiBytes(this, 0, count);
    } else if (canonicalCharsetName.equals("UTF-16BE")) {
        return CharsetUtils.toBigEndianUtf16Bytes(this, 0, count);
    } else {
        ByteBuffer buffer = charset.encode(this);
        byte[] bytes = new byte[buffer.limit()];
        buffer.get(bytes);
        return bytes;
    }
}

那这个方式和JNI得到的为什么不一样呢?经过查找发现,问题的根源竟是这样...(戳这里看原因)
先看下oracle给GetStringUTFChars的定义

GetStringUTFChars
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
该方法返回一个指向字节数组的指针,这个字节数组就是变种UTF-8(modified UTF-8)编码的string.
这个字节数组在ReleaseStringUTFChars()调用之前都是有效的.

关键点就是这个Modified UTF-8,那么它又是什么呢?

Modified UTF-8(变种UTF-8格式):
标准和变种的UTF-8有两个不同点。第一,空字符(null character,U+0000)使用双字节的0xc0 0x80,而不是单字节的0x00。这保证了在已编码字符串中没有嵌入空字节。因为C语言等语言程序中,单字节空字符是用来标志字符串结尾的。当已编码字符串放到这样的语言中处理,一个嵌入的空字符将把字符串一刀两断。
第二个不同点是基本多文种平面之外字符的编码的方法。在标准UTF-8中,这些字符使用4字节形式编码,而在改正的UTF-8中,这些字符和UTF-16一样首先表示为代理对(surrogate pairs),然后再像CESU-8那样按照代理对分别编码。这样改正的原因更是微妙。Java中的字符为16位长,因此一些Unicode字符需要两个Java字符来表示。语言的这个性质盖过了Unicode的增补平面的要求。尽管如此,为了要保持良好的向后兼容、要改变也不容易了。这个改正的编码系统保证了一个已编码字符串可以一次编为一个UTF-16码,而不是一次一个Unicode码点。不幸的是,这也意味着UTF-8中需要4字节的字符在变种UTF-8中变成需要6字节。
因为变种UTF-8并不是UTF-8,所以用户在交换信息和使用互联网的时候需要特别注意不要误把变种UTF-8当成UTF-8数据。(摘自维基百科

GetStringUTFChars得到的是一个修改过的UTF-8编码的字符串,那这个字符串到底有什么不同呢?
以笑脸Emoji表情为例(例子下面会给出Emoji表情是转化为UTF-8以及变种UTF-8形式字符串的计算方式):

😄 U+1F604
--> UTF-16格式:0xd83d 0xde04
--> UTF-8格式: 0xf0 0x9f 0x98 0x84
--> 变种UTF-8格式:0xed 0xa0 0xbd 0xed 0xb8 0x84

UTF-16转换方式

16进制编码范围 UTF-16表示方法(二进制) 10进制码范围 字节数量
U+0000-U+FFFF xxxxxxxx xxxxxxxx yyyyyyyy yyyyyyyy 0-65535 2
U+10000-U+10FFFF 110110yy yyyyyyyy 110111xx xxxxxxxx 65536-1114111 4

UTF-8转换格式

码点的位数 码点起值 码点终值 字节序列 Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6
7 U+0000 U+007F 1 0xxxxxxx
11 U+0080 U+07FF 2 110xxxxx 10xxxxxx
16 U+0800 U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx
21 U+10000 U+1FFFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
26 U+200000 U+3FFFFFF 5 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
31 U+4000000 U+7FFFFFFF 6 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

变种UTF-8格式的表示形式是如何得到的呢?

JNI使用modified UTF-8字符串表示各种string类型。所有在 \u0001\u007F范围内的字符都以1byte表示, 如下所示:


null字符 (\u0000) 以及在 \u0080\u07FF范围内的字符以一对byte(x和y)表示:

字符的值通过该式算出 ((x & 0x1f) << 6) + (y & 0x3f).
范围在 \u0800\uFFFF 内的字符由3个bytex, y, 和 z表示:

字符的值通过该式算出 ((x & 0xf) << 12) + ((y & 0x3f) << 6) + (z & 0x3f)。
超过U+FFFF的字符 (就是所谓的扩展字符) ,它们由UTF-16格式的代理码单元表示. 每个代理码单元由3个字节表示, 那就是说扩展字符由6个字节表示: u, v, w, x, y, 和z,计算方式为:0x10000+((v&0x0f)<<16)+((w&0x3f)<<10)+(y&0x0f)<<6)+(z&0x3f)

总结:使用API时要留心文档的细节,不要只是为了用而用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容