由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表示:
由Emoji表情发现的JNI GetStringUTFChars()隐藏的问题_第1张图片

字符的值通过该式算出 ((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)
由Emoji表情发现的JNI GetStringUTFChars()隐藏的问题_第2张图片

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

你可能感兴趣的:(由Emoji表情发现的JNI GetStringUTFChars()隐藏的问题)