由Emoji表情發(fā)現(xiàn)的JNI GetStringUTFChars()隱藏的問題

我們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é),不要只是為了用而用冗荸。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末承璃,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子俏竞,更是在濱河造成了極大的恐慌绸硕,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件魂毁,死亡現(xiàn)場離奇詭異玻佩,居然都是意外死亡,警方通過查閱死者的電腦和手機席楚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門咬崔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人烦秩,你說我怎么就攤上這事垮斯。” “怎么了只祠?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵兜蠕,是天一觀的道長。 經(jīng)常有香客問我抛寝,道長熊杨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任盗舰,我火速辦了婚禮晶府,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钻趋。我一直安慰自己川陆,他們只是感情好,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布蛮位。 她就那樣靜靜地躺著较沪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪土至。 梳的紋絲不亂的頭發(fā)上购对,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天,我揣著相機與錄音陶因,去河邊找鬼。 笑死垂蜗,一個胖子當著我的面吹牛楷扬,可吹牛的內(nèi)容都是我干的解幽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼烘苹,長吁一口氣:“原來是場噩夢啊……” “哼躲株!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起镣衡,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤霜定,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后廊鸥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體望浩,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年惰说,在試婚紗的時候發(fā)現(xiàn)自己被綠了磨德。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡吆视,死狀恐怖典挑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情啦吧,我是刑警寧澤您觉,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站授滓,受9級特大地震影響琳水,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜褒墨,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一炫刷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧郁妈,春花似錦浑玛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至胃碾,卻和暖如春涨享,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背仆百。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工厕隧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓吁讨,卻偏偏與公主長得像髓迎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子建丧,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容