我們App的消息收發(fā)底層由C++實現(xiàn),自然就需要使用JNI,開始的方案是將消息內(nèi)容String字符串直接向下傳梅掠,然后在JNI中解析為C++ string形式,
當然我們使用的是GetStringUTFChars
方法泽谨。然而消息發(fā)送后,發(fā)現(xiàn)Emoji表情在服務端無法正確解析。在java層和jni層分別加log后,我發(fā)現(xiàn)java層的消息內(nèi)容的16進制字符串與JNI使用GetStringUTFChars
方法得到C++格式string的16進制字符串內(nèi)容并不一樣简逮,我想這應該就是產(chǎn)生問題的原因,當然想法需要實際的驗證尿赚。
我更改了消息發(fā)送協(xié)議,在java層把消息內(nèi)容由String改為byte[]
數(shù)組形式蕉堰,這樣JNI層就不再需要使用GetStringUTFChars
方法轉(zhuǎn)換消息內(nèi)容凌净。再次測試,bingo屋讶,Emoji表情收發(fā)解析成功冰寻。
那不禁要問為什么會這樣呢?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得到的為什么不一樣呢轻腺?經(jīng)過查找發(fā)現(xiàn),問題的根源竟是這樣...(戳這里看原因)
先看下oracle給GetStringUTFChars
的定義
GetStringUTFChars
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
該方法返回一個指向字節(jié)數(shù)組的指針划乖,這個字節(jié)數(shù)組就是變種UTF-8(modified UTF-8)編碼的string.
這個字節(jié)數(shù)組在ReleaseStringUTFChars()調(diào)用之前都是有效的.
關(guān)鍵點就是這個Modified UTF-8
贬养,那么它又是什么呢?
Modified UTF-8(變種UTF-8格式):
標準和變種的UTF-8有兩個不同點琴庵。第一误算,空字符(null character,U+0000)使用雙字節(jié)的0xc0 0x80迷殿,而不是單字節(jié)的0x00儿礼。這保證了在已編碼字符串中沒有嵌入空字節(jié)。因為C語言等語言程序中庆寺,單字節(jié)空字符是用來標志字符串結(jié)尾的蚊夫。當已編碼字符串放到這樣的語言中處理,一個嵌入的空字符將把字符串一刀兩斷懦尝。
第二個不同點是基本多文種平面之外字符的編碼的方法这橙。在標準UTF-8中,這些字符使用4字節(jié)形式編碼导披,而在改正的UTF-8中屈扎,這些字符和UTF-16一樣首先表示為代理對(surrogate pairs),然后再像CESU-8那樣按照代理對分別編碼撩匕。這樣改正的原因更是微妙鹰晨。Java中的字符為16位長,因此一些Unicode字符需要兩個Java字符來表示止毕。語言的這個性質(zhì)蓋過了Unicode的增補平面的要求模蜡。盡管如此,為了要保持良好的向后兼容扁凛、要改變也不容易了忍疾。這個改正的編碼系統(tǒng)保證了一個已編碼字符串可以一次編為一個UTF-16碼,而不是一次一個Unicode碼點谨朝。不幸的是卤妒,這也意味著UTF-8中需要4字節(jié)的字符在變種UTF-8中變成需要6字節(jié)。
因為變種UTF-8并不是UTF-8字币,所以用戶在交換信息和使用互聯(lián)網(wǎng)的時候需要特別注意不要誤把變種UTF-8當成UTF-8數(shù)據(jù)则披。(摘自維基百科)
GetStringUTFChars
得到的是一個修改過的UTF-8編碼的字符串,那這個字符串到底有什么不同呢洗出?
以笑臉Emoji表情為例(例子下面會給出Emoji表情是轉(zhuǎn)化為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轉(zhuǎn)換方式:
16進制編碼范圍 | UTF-16表示方法(二進制) | 10進制碼范圍 | 字節(jié)數(shù)量 |
---|---|---|---|
U+0000-U+FFFF | xxxxxxxx xxxxxxxx yyyyyyyy yyyyyyyy | 0-65535 | 2 |
U+10000-U+10FFFF | 110110yy yyyyyyyy 110111xx xxxxxxxx | 65536-1114111 | 4 |
UTF-8轉(zhuǎn)換格式:
碼點的位數(shù) | 碼點起值 | 碼點終值 | 字節(jié)序列 | 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
范圍內(nèi)的字符都以1byte表示, 如下所示:
null字符 (\u0000
) 以及在\u0080
到\u07FF
范圍內(nèi)的字符以一對byte(x和y)表示:
字符的值通過該式算出 ((x & 0x1f) << 6) + (y & 0x3f).
范圍在\u0800
到\uFFFF
內(nèi)的字符由3個bytex, y, 和 z表示:
字符的值通過該式算出 ((x & 0xf) << 12) + ((y & 0x3f) << 6) + (z & 0x3f)。
超過U+FFFF的字符 (就是所謂的擴展字符) 翩活,它們由UTF-16格式的代理碼單元表示. 每個代理碼單元由3個字節(jié)表示阱洪, 那就是說擴展字符由6個字節(jié)表示: u, v, w, x, y, 和z便贵,計算方式為:0x10000+((v&0x0f)<<16)+((w&0x3f)<<10)+(y&0x0f)<<6)+(z&0x3f)
總結(jié):使用API時要留心文檔的細節(jié),不要只是為了用而用冗荸。