本文將從一個(gè) Native Crash 分析入手,帶大家了解一下我們平時(shí)開發(fā)中常用容易忽略但是又很值得學(xué)習(xí)底層源碼知識(shí)几迄。
一、問題起因
最近在項(xiàng)目中遇到一個(gè) native crash,引起 crash 的代碼如下所示:
jstring stringTojstring(JNIEnv* env, string str)
{
int len = str.length();
wchar_t *wcs = new wchar_t[len * 2];
int nRet = UTF82Unicode(str.c_str(), wcs, len);
jchar* jcs = new jchar[nRet];
for (int i = 0; i < nRet; i++)
{
jcs[i] = (jchar) wcs[i];
}
jstring retString = env->NewString(jcs, nRet);
delete[] wcs;
delete[] jcs;
return retString;
}
這段代碼的目的是用來將 c++ 里面的 string 類型轉(zhuǎn)成 jni 層的 jstring 對象呵恢,引發(fā)崩潰的代碼行是 env->NewString(jcs, nRet)
种蘸,最后跟蹤到的原因是 Native 層通過 env->CallIntMethod
的方式調(diào)用到了 Java 方法墓赴,而 Java 方法內(nèi)部拋出了 Exception竞膳,Native 層未及時(shí)通過 env->ExceptionClear
清除這個(gè)異常就直接調(diào)用了 stringTojstring
方法,最終導(dǎo)致 env->NewString(jcs, nRet)
這行代碼拋出異常诫硕。
二坦辟、代碼分析與問題發(fā)掘
這個(gè) crash 最后的解決方法是及時(shí)調(diào)用 env->ExceptionClear
清除這個(gè)異常即可≌掳欤回頭詳細(xì)分析這個(gè)函數(shù)锉走,新的疑惑就出現(xiàn)了,為什么會(huì)存在這么一個(gè)轉(zhuǎn)換函數(shù)藕届,我們知道將 c++ 里面的 string 類型轉(zhuǎn)成 jni 層的 jstring 類型有一個(gè)更加簡便的函數(shù) env->NewStringUTF(str.c_str())
挪蹭,為什么不直接調(diào)用這個(gè)函數(shù),而需要通過這么復(fù)雜的步驟進(jìn)行 string 到 jstring 的轉(zhuǎn)換休偶,接下來我們會(huì)仔細(xì)分析相關(guān)源碼來解答這個(gè)疑惑梁厉。先把相關(guān)的幾個(gè)函數(shù)源碼貼出來:
inline int UTF82UnicodeOne(const char* utf8, wchar_t& wch)
{
//首字符的Ascii碼大于0xC0才需要向后判斷,否則踏兜,就肯定是單個(gè)ANSI字符了
unsigned char firstCh = utf8[0];
if (firstCh >= 0xC0)
{
//根據(jù)首字符的高位判斷這是幾個(gè)字母的UTF8編碼
int afters, code;
if ((firstCh & 0xE0) == 0xC0)
{
afters = 2;
code = firstCh & 0x1F;
}
else if ((firstCh & 0xF0) == 0xE0)
{
afters = 3;
code = firstCh & 0xF;
}
else if ((firstCh & 0xF8) == 0xF0)
{
afters = 4;
code = firstCh & 0x7;
}
else if ((firstCh & 0xFC) == 0xF8)
{
afters = 5;
code = firstCh & 0x3;
}
else if ((firstCh & 0xFE) == 0xFC)
{
afters = 6;
code = firstCh & 0x1;
}
else
{
wch = firstCh;
return 1;
}
//知道了字節(jié)數(shù)量之后词顾,還需要向后檢查一下,如果檢查失敗碱妆,就簡單的認(rèn)為此UTF8編碼有問題肉盹,或者不是UTF8編碼,于是當(dāng)成一個(gè)ANSI來返回處理
for(int k = 1; k < afters; ++ k)
{
if ((utf8[k] & 0xC0) != 0x80)
{
//判斷失敗疹尾,不符合UTF8編碼的規(guī)則上忍,直接當(dāng)成一個(gè)ANSI字符返回
wch = firstCh;
return 1;
}
code <<= 6;
code |= (unsigned char)utf8[k] & 0x3F;
}
wch = code;
return afters;
}
else
{
wch = firstCh;
}
return 1;
}
int UTF82Unicode(const char* utf8Buf, wchar_t *pUniBuf, int utf8Leng)
{
int i = 0, count = 0;
while(i < utf8Leng)
{
i += UTF82UnicodeOne(utf8Buf + i, pUniBuf[count]);
count ++;
}
return count;
}
jstring stringTojstring(JNIEnv* env, string str)
{
int len = str.length();
wchar_t *wcs = new wchar_t[len * 2];
int nRet = UTF82Unicode(str.c_str(), wcs, len);
jchar* jcs = new jchar[nRet];
for (int i = 0; i < nRet; i++)
{
jcs[i] = (jchar) wcs[i];
}
jstring retString = env->NewString(jcs, nRet);
delete[] wcs;
delete[] jcs;
return retString;
}
由于無法找到代碼的出處和作者,所以現(xiàn)在我們只能通過源碼去推測意圖航棱。
首先我們先看第一個(gè)函數(shù) UTF82Unicode
睡雇,這個(gè)函數(shù)顧名思義是將 utf-8 編碼轉(zhuǎn)成 unicode(utf-16) 編碼。然后分析第二個(gè)函數(shù) UTF82UnicodeOne
饮醇,這個(gè)函數(shù)看起來會(huì)比較費(fèi)解它抱,因?yàn)檫@涉及到 utf-16 與 utf-8 編碼轉(zhuǎn)換的知識(shí),所以我們先來詳細(xì)了解一下這兩種常用編碼朴艰。
三观蓄、utf-16 與 utf-8 編碼
首先需要明確的一點(diǎn)是我們平時(shí)說的 unicode 編碼其實(shí)指的是 ucs-2 或者 utf-16 編碼,unicode 真正是一個(gè)業(yè)界標(biāo)準(zhǔn)祠墅,它對世界上大部分的文字系統(tǒng)進(jìn)行了整理侮穿、編碼,它只規(guī)定了符號的二進(jìn)制代碼毁嗦,卻沒有規(guī)定這個(gè)二進(jìn)制代碼應(yīng)該如何存儲(chǔ)亲茅。所以嚴(yán)格意義上講 utf-8、utf-16 和 ucs-2 編碼都是 unicode 字符集的一種實(shí)現(xiàn)方式,只不過前兩者是變長編碼克锣,后者則是定長茵肃。
utf-8 編碼最大的特點(diǎn)就是變長編碼,它使用 1~4 個(gè)字節(jié)來表示一個(gè)符號袭祟,根據(jù)符號不同動(dòng)態(tài)變換字節(jié)的長度验残;
ucs-2 編碼最大的特點(diǎn)就是定長編碼,它規(guī)定統(tǒng)一使用 2 個(gè)字節(jié)來表示一個(gè)符號巾乳;
utf-16 也是變長編碼您没,用 2 個(gè)或者 4 個(gè)字節(jié)來代表一個(gè)字符,在基本多文種平面集上和 ucs-2 表現(xiàn)一樣胆绊;
unicode 字符集是 ISO(國際標(biāo)準(zhǔn)化組織)國際組織推行的氨鹏,我們知道英文的 26 個(gè)字母加上其他的英文基本符號通過 ASCII 編碼就完全足夠了,可是像中文這種有上萬個(gè)字符的語種來說 ASCII 就完全不夠用了辑舷,所以為了統(tǒng)一全世界不同國家的編碼喻犁,他們廢了所有的地區(qū)性編碼方案槽片,重新收集了絕大多數(shù)文化中所有字母和符號的編碼何缓,命名為 "Universal Multiple-Octet Coded Character Set",簡稱 UCS, 俗稱 "unicode"还栓,unicode 與 utf-8 編碼的對應(yīng)關(guān)系:
Unicode符號范圍 | UTF-8編碼方式
(十六進(jìn)制) | (二進(jìn)制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
那么既然都已經(jīng)推出了 unicode 統(tǒng)一編碼字符集碌廓,為什么不統(tǒng)一全部使用 ucs-2/utf-16 編碼呢?這是因?yàn)槠鋵?shí)對于英文使用國家來說剩盒,字符基本上都是 ASCII 字符谷婆,使用 utf-8 編碼一個(gè)字節(jié)代表一個(gè)字符很常見,如果使用 ucs-2/utf-16 編碼反而會(huì)浪費(fèi)空間辽聊。
除了上面介紹到的幾種編碼方式纪挎,還有 utf-32 編碼,也被稱為 ucs-4 編碼跟匆,它對于每個(gè)字符統(tǒng)一使用 4 個(gè)字節(jié)來表示异袄。需要注意的是,utf-16 編碼是 ucs-2 編碼的擴(kuò)展(在 unicode 引入字符平面集概念之前玛臂,他們是一樣的)烤蜕,ucs-2 編碼在基本多文種平面字符集上和 utf-16 結(jié)果一致,但是 utf-16 編碼可以使用 4 個(gè)字節(jié)來表示基本多文種平面之外的字符集迹冤,前兩個(gè)字節(jié)稱為前導(dǎo)代理讽营,后兩個(gè)字節(jié)稱為后尾代理,這兩個(gè)代理構(gòu)成一個(gè)代理對泡徙。unicode 總共有 17 個(gè)字符平面集:
平面 | 始末字符值 | 中文名稱 | 英文名稱 |
---|---|---|---|
0號平面 | U+0000 - U+FFFF | 基本多文種平面 | BMP |
1號平面 | U+10000 - U+1FFFF | 多文種補(bǔ)充平面 | SMP |
2號平面 | U+20000 - U+2FFFF | 表意文字補(bǔ)充平面 | SIP |
3號平面 | U+30000 - U+3FFFF 表意文字第三平面 | TIP | |
4~13號平面 | U+40000 - U+DFFFF | (尚未使用) | |
14號平面 | U+E0000 - U+EFFFF | 特別用途補(bǔ)充平面 | SSP |
15號平面 | U+F0000 - U+FFFFF | 保留作為私人使用區(qū)(A區(qū)) | PUA-A |
16號平面 | U+100000 - U+10FFFF | 保留作為私人使用區(qū)(B區(qū)) | PUA-B |
通過上面介紹的內(nèi)容橱鹏,我們應(yīng)該基本了解了幾種編碼方式的概念和區(qū)別,其中最重要的是要記住 utf-8 編碼和 utf-16 編碼之間的轉(zhuǎn)換公式,后面我們馬上就會(huì)用到莉兰。
四狡蝶、NewString 與 NewStringUTF 源碼分析
我們回到上面的問題:為什么不直接使用 env->NewStringUTF
,而是需要先做一個(gè) utf-8 編碼到 utf-16 編碼的轉(zhuǎn)換贮勃,將轉(zhuǎn)換之后的值通過 env->NewString
生成一個(gè) jstring 呢贪惹?應(yīng)該可以確定是作者有意為之,于是我們下沉到源碼中去尋找問題的答案寂嘉。
因?yàn)?dalvik 和 ART 的行為表現(xiàn)是有差異的奏瞬,所以我們有必要來了解一下兩者的實(shí)現(xiàn):
4.1、 dalvik 源碼解析
首先我們來分析一下 dalvik 中這兩個(gè)函數(shù)的源碼泉孩,他們的調(diào)用時(shí)序如下圖所示:
可見硼端,NewString
和 NewStringUTF
的調(diào)用過程很相似,最大區(qū)別在于后者會(huì)有額外的 dvmConvertUtf8ToUtf16
操作寓搬,接下來我們按照流程剖析每一個(gè)方法的源碼珍昨。這兩個(gè)函數(shù)定義都在 jni.h 文件中,對應(yīng)的實(shí)現(xiàn)在 jni.cpp 文件中(這里選取的是 Android 4.3.1 的源碼):
/*
* Create a new String from Unicode data.
*/
static jstring NewString(JNIEnv* env, const jchar* unicodeChars, jsize len) {
ScopedJniThreadState ts(env);
StringObject* jstr = dvmCreateStringFromUnicode(unicodeChars, len);
if (jstr == NULL) {
return NULL;
}
dvmReleaseTrackedAlloc((Object*) jstr, NULL);
return (jstring) addLocalReference(ts.self(), (Object*) jstr);
}
....
/*
* Create a new java.lang.String object from chars in modified UTF-8 form.
*/
static jstring NewStringUTF(JNIEnv* env, const char* bytes) {
ScopedJniThreadState ts(env);
if (bytes == NULL) {
return NULL;
}
/* note newStr could come back NULL on OOM */
StringObject* newStr = dvmCreateStringFromCstr(bytes);
jstring result = (jstring) addLocalReference(ts.self(), (Object*) newStr);
dvmReleaseTrackedAlloc((Object*)newStr, NULL);
return result;
}
可以看到這兩個(gè)函數(shù)步驟是類似的句喷,先創(chuàng)建一個(gè) StringObject 對象镣典,然后將它加入到 localReference table 中。兩個(gè)函數(shù)的差別在于生成 StringObject 對象的函數(shù)不一樣唾琼, NewString
調(diào)用的是 dvmCreateStringFromUnicode
兄春,NewStringUTF
則調(diào)用了 dvmCreateStringFromCstr
。于是我們繼續(xù)分析 dvmCreateStringFromUnicode
和 dvmCreateStringFromCstr
這兩個(gè)函數(shù)锡溯,他們的實(shí)現(xiàn)是在 UtfString.c 中:
/*
* Create a new java/lang/String object, using the given Unicode data.
*/
StringObject* dvmCreateStringFromUnicode(const u2* unichars, int len)
{
/* We allow a NULL pointer if the length is zero. */
assert(len == 0 || unichars != NULL);
ArrayObject* chars;
StringObject* newObj = makeStringObject(len, &chars);
if (newObj == NULL) {
return NULL;
}
if (len > 0) memcpy(chars->contents, unichars, len * sizeof(u2));
u4 hashCode = computeUtf16Hash((u2*)(void*)chars->contents, len);
dvmSetFieldInt((Object*)newObj, STRING_FIELDOFF_HASHCODE, hashCode);
return newObj;
}
....
StringObject* dvmCreateStringFromCstr(const char* utf8Str) {
assert(utf8Str != NULL);
return dvmCreateStringFromCstrAndLength(utf8Str, dvmUtf8Len(utf8Str));
}
/*
* Create a java/lang/String from a C string, given its UTF-16 length
* (number of UTF-16 code points).
*/
StringObject* dvmCreateStringFromCstrAndLength(const char* utf8Str,
size_t utf16Length)
{
assert(utf8Str != NULL);
ArrayObject* chars;
StringObject* newObj = makeStringObject(utf16Length, &chars);
if (newObj == NULL) {
return NULL;
}
dvmConvertUtf8ToUtf16((u2*)(void*)chars->contents, utf8Str);
u4 hashCode = computeUtf16Hash((u2*)(void*)chars->contents, utf16Length);
dvmSetFieldInt((Object*) newObj, STRING_FIELDOFF_HASHCODE, hashCode);
return newObj;
}
這兩個(gè)函數(shù)流程類似赶舆,首先通過 makeStringObject
函數(shù)生成 StringObjcet 對象并且根據(jù)類型分配內(nèi)存,然后通過 memcpy
或者 dvmConvertUtf8ToUtf16
函數(shù)分別將 jchar 數(shù)組或者 char 數(shù)組的內(nèi)容設(shè)置到這個(gè)對象中祭饭,最后將計(jì)算好的 hash 值也設(shè)置到 StringObject 對象中芜茵。很明顯的區(qū)別就在于 memcpy
函數(shù)和 dvmConvertUtf8ToUtf16
函數(shù),我們對比一下這兩個(gè)函數(shù)倡蝙。
memcpy
函數(shù)這里就不分析了九串,內(nèi)存拷貝函數(shù),將 unichars 指向的 jchar 數(shù)組拷貝到 StringObject 內(nèi)容區(qū)域中悠咱;dvmConvertUtf8ToUtf16
函數(shù)我們仔細(xì)分析一下:
/*
* Convert a "modified" UTF-8 string to UTF-16.
*/
void dvmConvertUtf8ToUtf16(u2* utf16Str, const char* utf8Str)
{
while (*utf8Str != '\0')
*utf16Str++ = dexGetUtf16FromUtf8(&utf8Str);
}
通過注釋我們可以看到蒸辆,這個(gè)函數(shù)用來將 utf-8 編碼轉(zhuǎn)換成 utf-16 編碼,繼續(xù)跟到 dexGetUtf16FromUtf8
函數(shù)中析既,這個(gè)函數(shù)在 DexUtf.h 文件中:
/*
* Retrieve the next UTF-16 character from a UTF-8 string.
*/
DEX_INLINE u2 dexGetUtf16FromUtf8(const char** pUtf8Ptr)
{
unsigned int one, two, three;
one = *(*pUtf8Ptr)++;
if ((one & 0x80) != 0) {
/* two- or three-byte encoding */
two = *(*pUtf8Ptr)++;
if ((one & 0x20) != 0) {
/* three-byte encoding */
three = *(*pUtf8Ptr)++;
return ((one & 0x0f) << 12) |
((two & 0x3f) << 6) |
(three & 0x3f);
} else {
/* two-byte encoding */
return ((one & 0x1f) << 6) |
(two & 0x3f);
}
} else {
/* one-byte encoding */
return one;
}
}
這段代碼的核心就是我們上面提到的 utf-8 和 utf-16 轉(zhuǎn)換的公式躬贡。我們詳細(xì)解析一下這個(gè)函數(shù),先假設(shè)傳遞過來的字符串是“a中文”眼坏,對應(yīng) utf-8 編碼十六進(jìn)制是 "0x610xE40xB80xAD0xE60x960x87"拂玻,轉(zhuǎn)換步驟如下:
- 先執(zhí)行一個(gè)語句
one = *(*pUtf8Ptr)++;
將入?yún)?char** pUtf8Ptr
解引用酸些,獲取字符串指針,再解一次檐蚜,并將指針后移魄懂,其實(shí)就是獲取字符代表的 'a'(0x61),然后 0x61&0x80 = 0x00闯第,說明這是單字節(jié)的 utf-8 字符市栗,返回 0x61 給上層,由于上層是 u2(typedef uint16_t u2)咳短,所以上層將結(jié)果存儲(chǔ)為 0x000x61填帽; - 外層循環(huán)繼續(xù)執(zhí)行該函數(shù),走到了第二個(gè)字符 0xE4咙好,0xE4&0x80 = 0x80篡腌,表示其為雙字節(jié)或三字節(jié)的 utf-8 編碼,繼續(xù)走到下一個(gè)字節(jié) 0xB8勾效,0xB8&0x20 = 0x20嘹悼,代表是三字節(jié)編碼的 utf-8 編碼,然后執(zhí)行
((one & 0x0f) << 12) | ((two & 0x3f) << 6) | (three & 0x3f);
层宫,這個(gè)語句對應(yīng)的就是 utf-8 與 utf-16 的轉(zhuǎn)換公式杨伙,最后返回結(jié)果是 0x4E2D,這個(gè)也是 “中” 的 unicode 字符集卒密,返回給外層存儲(chǔ)為 0x4E2D; - 外層地址繼續(xù)往后自增缀台,再次執(zhí)行到該函數(shù)時(shí),one 字符就成了 0xE6哮奇,此時(shí)步驟和第二步類似,返回結(jié)果是 0x6587睛约,外層存儲(chǔ)為 0x6587鼎俘,代表 unicode 中的 “文”;
- 函數(shù)執(zhí)行完成后辩涝, utf-8 編碼就被轉(zhuǎn)成了 utf-16 編碼贸伐。
回顧整個(gè)過程我們可以發(fā)現(xiàn),NewString
和 NewStringUTF
生成的 jstring 對象都是 utf-16 編碼怔揩,所以這里我們可以得出一個(gè)推論:在 dalvik 虛擬機(jī)中捉邢,native 方法創(chuàng)建的 String 對象都是 utf-16 編碼。那么 Java 類中創(chuàng)建的 String 對象是什么編碼呢?其實(shí)也是 utf-16,后面我們會(huì)證實(shí)這個(gè)推論铡羡。
4.2 ART 源碼分析
分析完 dalvik 源碼之后蝶棋,我們來分析一下 ART 的相關(guān)源碼(這里選取的是 Android 8.0 源碼),同樣的流程捶枢,先是兩個(gè)函數(shù)的調(diào)用時(shí)序圖:
這兩個(gè)函數(shù)實(shí)現(xiàn)在 jni_internal.cc 文件中:
static jstring NewString(JNIEnv*env, const jchar*chars, jsize char_count) {
if (UNLIKELY(char_count < 0)) {
JavaVmExtFromEnv(env)->JniAbortF("NewString", "char_count < 0: %d", char_count);
return nullptr;
}
if (UNLIKELY(chars == nullptr && char_count > 0)) {
JavaVmExtFromEnv(env)->JniAbortF("NewString", "chars == null && char_count > 0");
return nullptr;
}
ScopedObjectAccess soa (env);
mirror::String * result = mirror::String::AllocFromUtf16(soa.Self(), char_count, chars);
return soa.AddLocalReference < jstring > (result);
}
...
static jstring NewStringUTF(JNIEnv*env, const char*utf) {
if (utf == nullptr) {
return nullptr;
}
ScopedObjectAccess soa (env);
mirror::String * result = mirror::String::AllocFromModifiedUtf8(soa.Self(), utf);
return soa.AddLocalReference < jstring > (result);
}
可以看到他們調(diào)用的函數(shù)分別是 AllocFromUtf16
和 AllocFromModifiedUtf8
赴魁,這兩個(gè)函數(shù)在 string.cc 文件中:
String*String::AllocFromUtf16(Thread*self, int32_t utf16_length, const uint16_t*utf16_data_in) {
CHECK(utf16_data_in != nullptr || utf16_length == 0);
gc::AllocatorType allocator_type = Runtime::Current () -> GetHeap()->GetCurrentAllocator();
const bool compressible = kUseStringCompression &&
String::AllASCII < uint16_t > (utf16_data_in, utf16_length);
int32_t length_with_flag = String::GetFlaggedCount (utf16_length, compressible);
SetStringCountVisitor visitor (length_with_flag);
ObjPtr<String> string = Alloc < true > (self, length_with_flag, allocator_type, visitor);
if (UNLIKELY(string == nullptr)) {
return nullptr;
}
if (compressible) {
for (int i = 0; i < utf16_length; ++i) {
string -> GetValueCompressed()[i] = static_cast < uint8_t > (utf16_data_in[i]);
}
} else {
uint16_t * array = string -> GetValue();
memcpy(array, utf16_data_in, utf16_length * sizeof(uint16_t));
}
return string.Ptr();
}
....
String* String::AllocFromModifiedUtf8(Thread* self, const char* utf) {
DCHECK(utf != nullptr);
size_t byte_count = strlen(utf);
size_t char_count = CountModifiedUtf8Chars(utf, byte_count);
return AllocFromModifiedUtf8(self, char_count, utf, byte_count);
}
String* String::AllocFromModifiedUtf8(Thread* self,
int32_t utf16_length,
const char* utf8_data_in,
int32_t utf8_length) {
gc::AllocatorType allocator_type = Runtime::Current()->GetHeap()->GetCurrentAllocator();
const bool compressible = kUseStringCompression && (utf16_length == utf8_length);
const int32_t utf16_length_with_flag = String::GetFlaggedCount(utf16_length, compressible);
SetStringCountVisitor visitor(utf16_length_with_flag);
ObjPtr<String> string = Alloc<true>(self, utf16_length_with_flag, allocator_type, visitor);
if (UNLIKELY(string == nullptr)) {
return nullptr;
}
if (compressible) {
memcpy(string->GetValueCompressed(), utf8_data_in, utf16_length * sizeof(uint8_t));
} else {
uint16_t* utf16_data_out = string->GetValue();
ConvertModifiedUtf8ToUtf16(utf16_data_out, utf16_length, utf8_data_in, utf8_length);
}
return string.Ptr();
}
CountModifiedUtf8Chars
和 ConvertModifiedUtf8ToUtf16
函數(shù)在 utf.cc 文件中:
/*
* This does not validate UTF8 rules (nor did older code). But it gets the right answer
* for valid UTF-8 and that's fine because it's used only to size a buffer for later
* conversion.
*
* Modified UTF-8 consists of a series of bytes up to 21 bit Unicode code points as follows:
* U+0001 - U+007F 0xxxxxxx
* U+0080 - U+07FF 110xxxxx 10xxxxxx
* U+0800 - U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
* U+10000 - U+1FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
*
* U+0000 is encoded using the 2nd form to avoid nulls inside strings (this differs from
* standard UTF-8).
* The four byte encoding converts to two utf16 characters.
*/
size_t CountModifiedUtf8Chars(const char* utf8, size_t byte_count) {
DCHECK_LE(byte_count, strlen(utf8));
size_t len = 0;
const char* end = utf8 + byte_count;
for (; utf8 < end; ++utf8) {
int ic = *utf8;
len++;
if (LIKELY((ic & 0x80) == 0)) {
// One-byte encoding.
continue;
}
// Two- or three-byte encoding.
utf8++;
if ((ic & 0x20) == 0) {
// Two-byte encoding.
continue;
}
utf8++;
if ((ic & 0x10) == 0) {
// Three-byte encoding.
continue;
}
// Four-byte encoding: needs to be converted into a surrogate
// pair.
utf8++;
len++;
}
return len;
}
void ConvertModifiedUtf8ToUtf16(uint16_t* utf16_data_out, size_t out_chars,
const char* utf8_data_in, size_t in_bytes) {
const char *in_start = utf8_data_in;
const char *in_end = utf8_data_in + in_bytes;
uint16_t *out_p = utf16_data_out;
if (LIKELY(out_chars == in_bytes)) {
// Common case where all characters are ASCII.
for (const char *p = in_start; p < in_end;) {
// Safe even if char is signed because ASCII characters always have
// the high bit cleared.
*out_p++ = dchecked_integral_cast<uint16_t>(*p++);
}
return;
}
// String contains non-ASCII characters.
for (const char *p = in_start; p < in_end;) {
const uint32_t ch = GetUtf16FromUtf8(&p);
const uint16_t leading = GetLeadingUtf16Char(ch);
const uint16_t trailing = GetTrailingUtf16Char(ch);
*out_p++ = leading;
if (trailing != 0) {
*out_p++ = trailing;
}
}
}
首先规惰, AllocFromUtf16
函數(shù)中是簡單的賦值或者 memcpy
操作吝镣,而 AllocFromModifiedUtf8
函數(shù)則是根據(jù) compressible
變量來選擇調(diào)用 memcpy
或者 ConvertModifiedUtf8ToUtf16
函數(shù)堤器。AllocFromUtf16
和 ConvertModifiedUtf8ToUtf16
這兩個(gè)函數(shù)中都有對 compressible
這個(gè)變量的判斷,看看這個(gè)變量的賦值過程末贾,首先是 AllocFromUtf16
函數(shù) :
const bool compressible = kUseStringCompression && String::AllASCII < uint16_t > (utf16_data_in, utf16_length)
Android 8.0 源碼中 kUseStringCompression
該變量設(shè)置的值為 TRUE闸溃,所以如果字符全是 ASCII 則 compressible
變量也為 TRUE,但是很重要的一點(diǎn)是 Android 8.0 以下并沒有針對 compressible
變量的判斷拱撵,所有邏輯統(tǒng)一執(zhí)行 ConvertModifiedUtf8ToUtf16
操作圈暗;再來看一下 AllocFromModifiedUtf8
函數(shù)對于 compressible
的賦值操作:
const bool compressible = kUseStringCompression && (utf16_length == utf8_length);
如果 utf-8 編碼的字符串中字符數(shù)和字節(jié)數(shù)相等,即字符串都是 utf-8 單字節(jié)字符裕膀,那么直接執(zhí)行 memcpy
函數(shù)進(jìn)行拷貝员串;如果不相等,即字符串不都是 utf-8 單字節(jié)字符昼扛,需要經(jīng)過函數(shù) ConvertModifiedUtf8ToUtf16
將 utf-8 編碼轉(zhuǎn)換成 utf-16 編碼〈缙耄現(xiàn)在我們來著重分析這個(gè)過程,AllocFromModifiedUtf8
對于存在非 ASCII 編碼的字符會(huì)執(zhí)行到下面的一個(gè) for 循環(huán)中抄谐,在循環(huán)中分別執(zhí)行了 GetUtf16FromUtf8
渺鹦、GetLeadingUtf16Char
和 GetTrailingUtf16Char
函數(shù),這三個(gè)函數(shù)在 utf-inl.h 中:
inline uint16_t GetTrailingUtf16Char(uint32_t maybe_pair) {
return static_cast<uint16_t>(maybe_pair >> 16);
}
inline uint16_t GetLeadingUtf16Char(uint32_t maybe_pair) {
return static_cast<uint16_t>(maybe_pair & 0x0000FFFF);
}
inline uint32_t GetUtf16FromUtf8(const char** utf8_data_in) {
const uint8_t one = *(*utf8_data_in)++;
if ((one & 0x80) == 0) {
// one-byte encoding
return one;
}
const uint8_t two = *(*utf8_data_in)++;
if ((one & 0x20) == 0) {
// two-byte encoding
return ((one & 0x1f) << 6) | (two & 0x3f);
}
const uint8_t three = *(*utf8_data_in)++;
if ((one & 0x10) == 0) {
return ((one & 0x0f) << 12) | ((two & 0x3f) << 6) | (three & 0x3f);
}
// Four byte encodings need special handling. We'll have
// to convert them into a surrogate pair.
const uint8_t four = *(*utf8_data_in)++;
// Since this is a 4 byte UTF-8 sequence, it will lie between
// U+10000 and U+1FFFFF.
//
// TODO: What do we do about values in (U+10FFFF, U+1FFFFF) ? The
// spec says they're invalid but nobody appears to check for them.
const uint32_t code_point = ((one & 0x0f) << 18) | ((two & 0x3f) << 12)
| ((three & 0x3f) << 6) | (four & 0x3f);
uint32_t surrogate_pair = 0;
// Step two: Write out the high (leading) surrogate to the bottom 16 bits
// of the of the 32 bit type.
surrogate_pair |= ((code_point >> 10) + 0xd7c0) & 0xffff;
// Step three : Write out the low (trailing) surrogate to the top 16 bits.
surrogate_pair |= ((code_point & 0x03ff) + 0xdc00) << 16;
return surrogate_pair;
}
GetUtf16FromUtf8
函數(shù)首先判斷字符是幾個(gè)字節(jié)編碼蛹含,如果是四字節(jié)編碼需要特殊處理毅厚,轉(zhuǎn)換成代理對(surrogate pair);
GetTrailingUtf16Char
和 GetLeadingUtf16Char
邏輯就很簡單了浦箱,獲取返回字符串的低兩位字節(jié)和高兩位字節(jié)吸耿,如果高兩位字節(jié)不為空就組合成一個(gè)四字節(jié) utf-16 編碼的字符并返回。所以最后得出的結(jié)論就是:AllocFromModifiedUtf8
函數(shù)返回的結(jié)果要么全是 ASCII 字符的 utf-8 編碼字符串酷窥,要么就是 utf-16 編碼的字符串咽安。
分析到此處,我們可以知道 Android 8.0 及以上版本蓬推,在 Native 層創(chuàng)建 String 對象時(shí)妆棒,如果內(nèi)容全部為 ASCII 字符,String 就是 utf-8 編碼沸伏,否則為 utf-16 編碼糕珊。那么通過 Java 層創(chuàng)建的 String 對象呢?其實(shí)和從 Native 層創(chuàng)建的 String 對象情況一致毅糟,接下來我們會(huì)驗(yàn)證红选。
五、 推論驗(yàn)證
上面我們提出了兩個(gè)推論:
- Dalvik 中留特,String 對象編碼方式為 utf-16 編碼纠脾;
- ART 中玛瘸,String 對象編碼方式為 utf-16 編碼,但是有一個(gè)情況除外:如果 String 對象全部為 ASCII 字符并且 Android 系統(tǒng)為 8.0 及之上版本苟蹈,String 對象的編碼則為 utf-8糊渊;
為了驗(yàn)證上面的推論,我們用兩種方式來論證:
5.1慧脱、 獲取 String 對象中字符占用字節(jié)數(shù)
首先想到最直接的方式就是在 Android 4.3 的手機(jī)上獲取一個(gè) String 字符串的占用字節(jié)數(shù)渺绒,測試代碼如下所示:
String str = "hello from jni中文";
byte[] bytes = str.getBytes();
最后觀察一下 byte[] 數(shù)組的大小,最后發(fā)現(xiàn)是 20菱鸥,并不是 32宗兼,也就是說該字符串是 utf-8 編碼,并不是 utf-16 編碼氮采,和之前得出的結(jié)論不一致殷绍;我們同樣在 Android 6.0 手機(jī)上執(zhí)行相同的代碼,發(fā)現(xiàn)大小同樣是 20鹊漠。具體什么原因呢主到,我們來看一下 getBytes 源碼(分別在 String.java 與 Charset.java 類中):
/**
* Encodes this {@code String} into a sequence of bytes using the
* platform's default charset, storing the result into a new byte array.
*
* <p> The behavior of this method when this string cannot be encoded in
* the default charset is unspecified. The {@link
* java.nio.charset.CharsetEncoder} class should be used when more control
* over the encoding process is required.
*
* @return The resultant byte array
*
* @since JDK1.1
*/
public byte[] getBytes() {
return getBytes(Charset.defaultCharset());
}
/**
* Returns the default charset of this Java virtual machine.
*
* <p>Android note: The Android platform default is always UTF-8.
*
* @return A charset object for the default charset
*
* @since 1.5
*/
public static Charset defaultCharset() {
// Android-changed: Use UTF_8 unconditionally.
synchronized (Charset.class) {
if (defaultCharset == null) {
defaultCharset = java.nio.charset.StandardCharsets.UTF_8;
}
return defaultCharset;
}
}
通過源碼已經(jīng)可以清晰的看到使用 getBytes 函數(shù)獲取的是 utf-8 編碼的字符串。那么我們怎么知曉 Java 層 String 真正的編碼格式呢躯概,可不可以直接查看對象的內(nèi)存占用登钥?我們來試一下,通過 Android Profiler 的 Dump Java Heap 功能我們可以清楚的看到一個(gè)對象占用的內(nèi)存娶靡,首先通過 String str = "hello from jni中文"
代碼簡單的創(chuàng)建一個(gè) String 對象牧牢,然后通過 Android Profiler 工具查看這個(gè)對象的內(nèi)存占用,切換到 App Heap
與 Arrange by callstack
姿锭,找到創(chuàng)建的 String 對象:
可以看到對象占用大小是 48 個(gè)字節(jié)塔鳍,其中 char 數(shù)組占用的字節(jié)是 32,每個(gè)字符都是占用兩字節(jié)艾凯,這個(gè)行為在 Android 8.0 之前的版本一致献幔,所以我們可以很明確地推斷在 Android 8.0 之前通過上述方式創(chuàng)建的 String 對象都是 utf-16 編碼。
另外我們同時(shí)驗(yàn)證一下在 Android 8.0 版本及以上全為 ASCII 字符的 String 對象內(nèi)存占用詳細(xì)情況趾诗,測試代碼為 String output = "hello from jni"
:
可以看到占用字節(jié)數(shù)是 14,也就是單字節(jié)的 utf-8 編碼蹬蚁,所以我們的推論 2 也成立恃泪。
上面分析完通過 String str = "hello from jni中文"
方式創(chuàng)建的 String 對象是 utf-16 編碼,另外犀斋,String 對象還有一種創(chuàng)建方式:通過 new String(byte[] bytes)
贝乎,我們來直接分析源碼:
public String(byte[] data, int high, int offset, int byteCount) {
if ((offset | byteCount) < 0 || byteCount > data.length - offset) {
throw failedBoundsCheck(data.length, offset, byteCount);
}
this.offset = 0;
this.value = new char[byteCount];
this.count = byteCount;
high <<= 8;
for (int i = 0; i < count; i++) {
value[i] = (char) (high + (data[offset++] & 0xff));
}
}
通過代碼我們可以知道,因?yàn)?char 為雙字節(jié)叽粹,high 對應(yīng)的是高位字節(jié)览效,(data[offset++] & 0xff)
則為低位字節(jié)却舀,所以我們可以得出結(jié)論,String 對象通過這種情況下創(chuàng)建的同樣是 utf-16 編碼锤灿。
5.2挽拔、 官方資料
通過 5.1 小節(jié)的分析,我們已經(jīng)可以通過實(shí)際表現(xiàn)來支撐我們上面的兩點(diǎn)推論但校,作為補(bǔ)充螃诅,我們同時(shí)查閱相關(guān)官方資料來對這些推論得到更加全面的認(rèn)識(shí):
一、 How is text represented in the Java platform?
The Java programming language is based on the Unicode character set, and several libraries implement the Unicode standard. Unicode is an international character set standard which supports all of the major scripts of the world, as well as common technical symbols. The original Unicode specification defined characters as fixed-width 16-bit entities, but the Unicode standard has since been changed to allow for characters whose representation requires more than 16 bits. The range of legal code points is now U+0000 to U+10FFFF. An encoding defined by the standard, UTF-16, allows to represent all Unicode code points using one or two 16-bit units.
The primitive data type char in the Java programming language is an unsigned 16-bit integer that can represent a Unicode code point in the range U+0000 to U+FFFF, or the code units of UTF-16. The various types and classes in the Java platform that represent character sequences - char[], implementations of java.lang.CharSequence (such as the String class), and implementations of java.text.CharacterIterator - are UTF-16 sequences. Most Java source code is written in ASCII, a 7-bit character encoding, or ISO-8859-1, an 8-bit character encoding, but is translated into UTF-16 before processing.
The Character class as an object wrapper for the char primitive type. The Character class also contains static methods such as isLowerCase() and isDigit() for determining the properties of a character. Since J2SE 5, these methods have overloads that accept either a char (which allows representation of Unicode code points in the range U+0000 to U+FFFF) or an int (which allows representation of all Unicode code points).
我們重點(diǎn)看這一句
The various types and classes in the Java platform that represent character sequences - char[], implementations of java.lang.CharSequence (such as the String class), and implementations of java.text.CharacterIterator - are UTF-16 sequences.
String 類是實(shí)現(xiàn)了 CharSequence 接口状囱,所以自然而然是 utf-16 編碼术裸;
-XX:+UseCompressedStrings
Use a byte[] for Strings which can be represented as pure ASCII. (Introduced in Java 6 Update 21 Performance Release)
這個(gè)選項(xiàng)就是和上面的 kUseStringCompression
變量對應(yīng)亭枷。
六. 最后結(jié)論
經(jīng)過上面的分析我們可以得出以下結(jié)論:
- Dalvik 中 String 對象編碼方式為 utf-16 編碼袭艺;
- ART 中 String 對象編碼方式為 utf-16 編碼,但是有一個(gè)情況例外:如果 String 對象全部為 ASCII 字符并且 Android 系統(tǒng)為 8.0 及之上叨粘,String 對象的編碼則為 utf-8猾编;
- Android dalvik 中 utf-8 編碼轉(zhuǎn) utf-16 編碼的函數(shù)有缺陷,沒有對 4 字節(jié)的 utf-8 編碼做特殊處理宣鄙,直到 ART 中才對該缺陷進(jìn)行了修復(fù)袍镀。
6.1、 結(jié)論 3 驗(yàn)證
結(jié)論 3 就回答了我們最早的那個(gè)疑問冻晤,這個(gè)結(jié)論需要做一個(gè)簡單的比較分析苇羡。我們回到最上面的問題:為什么不直接使用 env->NewStringUTF()
函數(shù)進(jìn)行轉(zhuǎn)換,而需要額外寫一個(gè) UTF82UnicodeOne
函數(shù)鼻弧。其實(shí)細(xì)心的人可能已經(jīng)注意到了设江,上面 dalvik 和 ART 源碼中 utf-8 到 utf-16 轉(zhuǎn)換函數(shù)是有區(qū)別的,我們把關(guān)鍵代碼放到一起來進(jìn)行對比:
dalvik:
DEX_INLINE u2 dexGetUtf16FromUtf8(const char** pUtf8Ptr)
{
unsigned int one, two, three;
one = *(*pUtf8Ptr)++;
if ((one & 0x80) != 0) {
/* two- or three-byte encoding */
two = *(*pUtf8Ptr)++;
if ((one & 0x20) != 0) {
/* three-byte encoding */
three = *(*pUtf8Ptr)++;
return ((one & 0x0f) << 12) |
((two & 0x3f) << 6) |
(three & 0x3f);
} else {
/* two-byte encoding */
return ((one & 0x1f) << 6) |
(two & 0x3f);
}
} else {
/* one-byte encoding */
return one;
}
}
ART:
inline uint16_t GetTrailingUtf16Char(uint32_t maybe_pair) {
return static_cast<uint16_t>(maybe_pair >> 16);
}
inline uint16_t GetLeadingUtf16Char(uint32_t maybe_pair) {
return static_cast<uint16_t>(maybe_pair & 0x0000FFFF);
}
inline uint32_t GetUtf16FromUtf8(const char** utf8_data_in) {
const uint8_t one = *(*utf8_data_in)++;
if ((one & 0x80) == 0) {
// one-byte encoding
return one;
}
const uint8_t two = *(*utf8_data_in)++;
if ((one & 0x20) == 0) {
// two-byte encoding
return ((one & 0x1f) << 6) | (two & 0x3f);
}
const uint8_t three = *(*utf8_data_in)++;
if ((one & 0x10) == 0) {
return ((one & 0x0f) << 12) | ((two & 0x3f) << 6) | (three & 0x3f);
}
// Four byte encodings need special handling. We'll have
// to convert them into a surrogate pair.
const uint8_t four = *(*utf8_data_in)++;
// Since this is a 4 byte UTF-8 sequence, it will lie between
// U+10000 and U+1FFFFF.
//
// TODO: What do we do about values in (U+10FFFF, U+1FFFFF) ? The
// spec says they're invalid but nobody appears to check for them.
const uint32_t code_point = ((one & 0x0f) << 18) | ((two & 0x3f) << 12)
| ((three & 0x3f) << 6) | (four & 0x3f);
uint32_t surrogate_pair = 0;
// Step two: Write out the high (leading) surrogate to the bottom 16 bits
// of the of the 32 bit type.
surrogate_pair |= ((code_point >> 10) + 0xd7c0) & 0xffff;
// Step three : Write out the low (trailing) surrogate to the top 16 bits.
surrogate_pair |= ((code_point & 0x03ff) + 0xdc00) << 16;
return surrogate_pair;
}
發(fā)現(xiàn)了么攘轩?dalvik 代碼中并沒有對 4 字節(jié) utf-8 編碼的字符串進(jìn)行處理叉存,而 ART 中專門用了很詳細(xì)的注釋說明了針對 4 字節(jié)編碼的 utf-8 需要轉(zhuǎn)成代理對(surrogate pair)!為什么之前 Android 版本沒有針對 4 字節(jié)編碼進(jìn)行處理度帮?我的一個(gè)推測是:可能老版本的 Android 系統(tǒng)使用的是 ucs-2 編碼歼捏,并沒有對 BMP 之外的平面集做處理,所以也不存在 4 字節(jié)的 utf-8笨篷,在擴(kuò)展為 utf-16 編碼之后瞳秽,自然而然就需要額外對 4 字節(jié)的 utf-8 進(jìn)行轉(zhuǎn)換成代理對的操作。
測試這個(gè)結(jié)論也很簡單率翅,比如 "??" 是 4 字節(jié) utf-8 編碼字符(“??” 的 utf-8 編碼為 F0A0B296练俐,在線查詢網(wǎng)站:Unicode和UTF編碼轉(zhuǎn)換),在 Android 4.3 上通過 env->NewStringUTF
的方式轉(zhuǎn)換之后會(huì)出現(xiàn)崩潰冕臭,在 Android 6.0 上則可以正常轉(zhuǎn)換并且交給 Java 層展示腺晾,測試代碼如下:
char* c_str = new char[5];
c_str[0] = 0xF0;//“??”
c_str[1] = 0xA0;
c_str[2] = 0xB2;
c_str[3] = 0x96;
c_str[4] = 0x00;//end
__android_log_print(ANDROID_LOG_INFO, "jni", "%s", c_str);
return /*stringTojstring(env, temp)*/env->NewStringUTF(c_str);
如果在 Android 4.3 上將 env->NewStringUTF
替換成 stringTojstring
函數(shù)燕锥,就不會(huì)運(yùn)行崩潰了。雖然不會(huì)崩潰悯蝉,但是將轉(zhuǎn)換之后的 String 對象交給 Java 層卻顯示成亂碼归形,這是因?yàn)?stringTojstring
函數(shù)中并沒有針對 4 字節(jié)編碼的 utf-8 字符轉(zhuǎn)換成代理對,解決辦法可以參考 ART 的 GetUtf16FromUtf8
函數(shù)泉粉,感興趣的讀者可以自己實(shí)踐一下连霉。
經(jīng)過上面的測試,我們做一個(gè)推測嗡靡,UTF82UnicodeOne
函數(shù)的作者發(fā)現(xiàn)了上面我們描述的行為差異或者因?yàn)檫@個(gè)差異所引發(fā)的一些問題跺撼,才自己專門寫了這個(gè) stringTojstring
函數(shù)做轉(zhuǎn)換,針對 4 字節(jié)(5 字節(jié)和 6 字節(jié)的處理多余)編碼的 utf-8 進(jìn)行了單獨(dú)處理讨彼。
七歉井、引用
JavaScript 的內(nèi)部字符編碼是 UCS-2 還是 UTF-16
Dalvik虛擬機(jī)中NewStringUTF的實(shí)現(xiàn)