1. 程式人生 > 其它 >Android Native 開發之 NewString 與 NewStringUtf 解析

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 中這兩個函式的原始碼,他們的呼叫時序如下圖所示:

可見,NewStringNewStringUTF 的呼叫過程很相似,最大區別在於後者會有額外的 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 呼叫的是 dvmCreateStringFromUnicodeNewStringUTF 則呼叫了 dvmCreateStringFromCstr。於是我們繼續分析 dvmCreateStringFromUnicodedvmCreateStringFromCstr 這兩個函式,他們的實現是在 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 != '