Android Native 開發之 NewString 與 NewStringUtf 解析
字串是軟體開發中最為常見的物件之一,同時在Android開發中,其在Java和Native層之間傳遞也是一個高頻場景,本文將從一個 Native Crash 分析入手,帶大家瞭解我們平時開發中,那些容易忽略但又很值得學習的底層原始碼知識。 責任編輯:haodongyuan
一、問題起因
最近在專案中遇到一個 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 型別轉成 jni 層的 jstring 物件,引發崩潰的程式碼行是 env->NewString(jcs, nRet)
,最後跟蹤到的原因是 Native 層通過 env->CallIntMethod
的方式呼叫到了 Java 方法,而 Java 方法內部丟擲了 Exception,Native 層未及時通過 env->ExceptionClear
清除這個異常就直接呼叫了 stringTojstring
方法,最終導致 env->NewString(jcs, nRet)
這行程式碼丟擲異常。
二、程式碼分析與問題發掘
這個 crash 最後的解決方法是及時呼叫 env->ExceptionClear
清除這個異常即可。回頭詳細分析這個函式,新的疑惑就出現了,為什麼會存在這麼一個轉換函式,我們知道將 c++ 裡面的 string 型別轉成 jni 層的 jstring 型別有一個更加簡便的函式 env->NewStringUTF(str.c_str())
,為什麼不直接呼叫這個函式,而需要通過這麼複雜的步驟進行 string 到 jstring 的轉換,接下來我們會仔細分析相關原始碼來解答這個疑惑。先把相關的幾個函式原始碼貼出來:
inline int UTF82UnicodeOne(const char* utf8, wchar_t& wch) { //首字元的Ascii碼大於0xC0才需要向後判斷,否則,就肯定是單個ANSI字元了 unsigned char firstCh = utf8[0]; if (firstCh >= 0xC0) { //根據首字元的高位判斷這是幾個字母的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; } //知道了位元組數量之後,還需要向後檢查一下,如果檢查失敗,就簡單的認為此UTF8編碼有問題,或者不是UTF8編碼,於是當成一個ANSI來返回處理 for(int k = 1; k < afters; ++ k) { if ((utf8[k] & 0xC0) != 0x80) { //判斷失敗,不符合UTF8編碼的規則,直接當成一個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; }
由於無法找到程式碼的出處和作者,所以現在我們只能通過原始碼去推測意圖。
首先我們先看第一個函式 UTF82Unicode
,這個函式顧名思義是將 utf-8 編碼轉成 unicode(utf-16) 編碼。然後分析第二個函式 UTF82UnicodeOne
,這個函式看起來會比較費解,因為這涉及到 utf-16 與 utf-8 編碼轉換的知識,所以我們先來詳細瞭解一下這兩種常用編碼。
三、utf-16 與 utf-8 編碼
首先需要明確的一點是我們平時說的 unicode 編碼其實指的是 ucs-2 或者 utf-16 編碼,unicode 真正是一個業界標準,它對世界上大部分的文字系統進行了整理、編碼,它只規定了符號的二進位制程式碼,卻沒有規定這個二進位制程式碼應該如何儲存。所以嚴格意義上講 utf-8、utf-16 和 ucs-2 編碼都是 unicode 字符集的一種實現方式,只不過前兩者是變長編碼,後者則是定長。
utf-8 編碼最大的特點就是變長編碼,它使用 1~4 個位元組來表示一個符號,根據符號不同動態變換位元組的長度; ucs-2 編碼最大的特點就是定長編碼,它規定統一使用 2 個位元組來表示一個符號; utf-16 也是變長編碼,用 2 個或者 4 個位元組來代表一個字元,在基本多文種平面集上和 ucs-2 表現一樣; unicode 字符集是 ISO(國際標準化組織)國際組織推行的,我們知道英文的 26 個字母加上其他的英文基本符號通過 ASCII 編碼就完全足夠了,可是像中文這種有上萬個字元的語種來說 ASCII 就完全不夠用了,所以為了統一全世界不同國家的編碼,他們廢了所有的地區性編碼方案,重新收集了絕大多數文化中所有字母和符號的編碼,命名為 “Universal Multiple-Octet Coded Character Set”,簡稱 UCS, 俗稱 “unicode”,unicode 與 utf-8 編碼的對應關係:
Unicode符號範圍 | UTF-8編碼方式
(十六進位制) | (二進位制)
--------------------+---------------------------------------------
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
那麼既然都已經推出了 unicode 統一編碼字符集,為什麼不統一全部使用 ucs-2/utf-16 編碼呢?這是因為其實對於英文使用國家來說,字元基本上都是 ASCII 字元,使用 utf-8 編碼一個位元組代表一個字元很常見,如果使用 ucs-2/utf-16 編碼反而會浪費空間。
除了上面介紹到的幾種編碼方式,還有 utf-32 編碼,也被稱為 ucs-4 編碼,它對於每個字元統一使用 4 個位元組來表示。需要注意的是,utf-16 編碼是 ucs-2 編碼的擴充套件(在 unicode 引入字元平面集概念之前,他們是一樣的),ucs-2 編碼在基本多文種平面字符集上和 utf-16 結果一致,但是 utf-16 編碼可以使用 4 個位元組來表示基本多文種平面之外的字符集,前兩個位元組稱為前導代理,後兩個位元組稱為後尾代理,這兩個代理構成一個代理對。unicode 總共有 17 個字元平面集:
平面 |
始末字元值 |
中文名稱 |
英文名稱 |
---|---|---|---|
0號平面 |
U+0000 - U+FFFF |
基本多文種平面 |
BMP |
1號平面 |
U+10000 - U+1FFFF |
多文種補充平面 |
SMP |
2號平面 |
U+20000 - U+2FFFF |
表意文字補充平面 |
SIP |
3號平面 |
U+30000 - U+3FFFF 表意文字第三平面 |
TIP |
|
4~13號平面 |
U+40000 - U+DFFFF |
(尚未使用) |
|
14號平面 |
U+E0000 - U+EFFFF |
特別用途補充平面 |
SSP |
15號平面 |
U+F0000 - U+FFFFF |
保留作為私人使用區(A區) |
PUA-A |
16號平面 |
U+100000 - U+10FFFF |
保留作為私人使用區(B區) |
PUA-B |
通過上面介紹的內容,我們應該基本瞭解了幾種編碼方式的概念和區別,其中最重要的是要記住 utf-8 編碼和 utf-16 編碼之間的轉換公式,後面我們馬上就會用到。
四、NewString 與 NewStringUTF 原始碼分析
我們回到上面的問題:為什麼不直接使用 env->NewStringUTF
,而是需要先做一個 utf-8 編碼到 utf-16 編碼的轉換,將轉換之後的值通過 env->NewString
生成一個 jstring 呢?應該可以確定是作者有意為之,於是我們下沉到原始碼中去尋找問題的答案。
因為 dalvik 和 ART 的行為表現是有差異的,所以我們有必要來了解一下兩者的實現:
4.1、 dalvik 原始碼解析
首先我們來分析一下 dalvik 中這兩個函式的原始碼,他們的呼叫時序如下圖所示:
可見,NewString
和 NewStringUTF
的呼叫過程很相似,最大區別在於後者會有額外的 dvmConvertUtf8ToUtf16
操作,接下來我們按照流程剖析每一個方法的原始碼。這兩個函式定義都在 jni.h 檔案中,對應的實現在 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;
}
可以看到這兩個函式步驟是類似的,先建立一個 StringObject 物件,然後將它加入到 localReference table 中。兩個函式的差別在於生成 StringObject 物件的函式不一樣, NewString
呼叫的是 dvmCreateStringFromUnicode
,NewStringUTF
則呼叫了 dvmCreateStringFromCstr
。於是我們繼續分析 dvmCreateStringFromUnicode
和 dvmCreateStringFromCstr
這兩個函式,他們的實現是在 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;
}
這兩個函式流程類似,首先通過 makeStringObject
函式生成 StringObjcet 物件並且根據型別分配記憶體,然後通過 memcpy
或者 dvmConvertUtf8ToUtf16
函式分別將 jchar 陣列或者 char 陣列的內容設定到這個物件中,最後將計算好的 hash 值也設定到 StringObject 物件中。很明顯的區別就在於 memcpy
函式和 dvmConvertUtf8ToUtf16
函式,我們對比一下這兩個函式。
memcpy
函式這裡就不分析了,記憶體拷貝函式,將 unichars 指向的 jchar 陣列拷貝到 StringObject 內容區域中;dvmConvertUtf8ToUtf16
函式我們仔細分析一下:
/*
* Convert a "modified" UTF-8 string to UTF-16.
*/
void dvmConvertUtf8ToUtf16(u2* utf16Str, const char* utf8Str)
{
while (*utf8Str != '