Android Native 開發之 NewString 與 NewStringUtf 解析

Shawn_Dut發表於2019-02-28

本文將從一個 Native Crash 分析入手,帶大家瞭解一下我們平時開發中常用容易忽略但是又很值得學習底層原始碼知識。

一、問題起因

最近在專案中遇到一個 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 中這兩個函式的原始碼,他們的呼叫時序如下圖所示:

dvk_NewString.png
dvk_NewStringUTF.png

可見,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 != ` `)
        *utf16Str++ = dexGetUtf16FromUtf8(&utf8Str);
}
複製程式碼

通過註釋我們可以看到,這個函式用來將 utf-8 編碼轉換成 utf-16 編碼,繼續跟到 dexGetUtf16FromUtf8 函式中,這個函式在 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 轉換的公式。我們詳細解析一下這個函式,先假設傳遞過來的字串是“a中文”,對應 utf-8 編碼十六進位制是 “0x610xE40xB80xAD0xE60x960x87″,轉換步驟如下:

  1. 先執行一個語句 one = *(*pUtf8Ptr)++; 將入參 char** pUtf8Ptr 解引用,獲取字串指標,再解一次,並將指標後移,其實就是獲取字元代表的 `a`(0x61),然後 0x61&0x80 = 0x00,說明這是單位元組的 utf-8 字元,返回 0x61 給上層,由於上層是 u2(typedef uint16_t u2),所以上層將結果儲存為 0x000x61;
  2. 外層迴圈繼續執行該函式,走到了第二個字元 0xE4,0xE4&0x80 = 0x80,表示其為雙位元組或三位元組的 utf-8 編碼,繼續走到下一個位元組 0xB8,0xB8&0x20 = 0x20,代表是三位元組編碼的 utf-8 編碼,然後執行 ((one & 0x0f) << 12) | ((two & 0x3f) << 6) | (three & 0x3f);,這個語句對應的就是 utf-8 與 utf-16 的轉換公式,最後返回結果是 0x4E2D,這個也是 “中” 的 unicode 字符集,返回給外層儲存為 0x4E2D;
  3. 外層地址繼續往後自增,再次執行到該函式時,one 字元就成了 0xE6,此時步驟和第二步類似,返回結果是 0x6587,外層儲存為 0x6587,代表 unicode 中的 “文”;
  4. 函式執行完成後, utf-8 編碼就被轉成了 utf-16 編碼。

回顧整個過程我們可以發現,NewStringNewStringUTF 生成的 jstring 物件都是 utf-16 編碼,所以這裡我們可以得出一個推論:在 dalvik 虛擬機器中,native 方法建立的 String 物件都是 utf-16 編碼。那麼 Java 類中建立的 String 物件是什麼編碼呢?其實也是 utf-16,後面我們會證實這個推論。

4.2 ART 原始碼分析

分析完 dalvik 原始碼之後,我們來分析一下 ART 的相關原始碼(這裡選取的是 Android 8.0 原始碼),同樣的流程,先是兩個函式的呼叫時序圖:

art_NewString.png
art_NewStringUTF.png

這兩個函式實現在 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);
}
複製程式碼

可以看到他們呼叫的函式分別是 AllocFromUtf16AllocFromModifiedUtf8,這兩個函式在 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();
}
複製程式碼

CountModifiedUtf8CharsConvertModifiedUtf8ToUtf16 函式在 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 函式中是簡單的賦值或者 memcpy 操作,而 AllocFromModifiedUtf8 函式則是根據 compressible 變數來選擇呼叫 memcpy 或者 ConvertModifiedUtf8ToUtf16 函式。AllocFromUtf16ConvertModifiedUtf8ToUtf16 這兩個函式中都有對 compressible 這個變數的判斷,看看這個變數的賦值過程,首先是 AllocFromUtf16 函式 :

const bool compressible = kUseStringCompression && String::AllASCII < uint16_t > (utf16_data_in, utf16_length)
複製程式碼

Android 8.0 原始碼中 kUseStringCompression 該變數設定的值為 TRUE,所以如果字元全是 ASCII 則 compressible 變數也為 TRUE,但是很重要的一點是 Android 8.0 以下並沒有針對 compressible 變數的判斷,所有邏輯統一執行 ConvertModifiedUtf8ToUtf16 操作;再來看一下 AllocFromModifiedUtf8 函式對於 compressible 的賦值操作:

const bool compressible = kUseStringCompression && (utf16_length == utf8_length);
複製程式碼

如果 utf-8 編碼的字串中字元數和位元組數相等,即字串都是 utf-8 單位元組字元,那麼直接執行 memcpy 函式進行拷貝;如果不相等,即字串不都是 utf-8 單位元組字元,需要經過函式 ConvertModifiedUtf8ToUtf16 將 utf-8 編碼轉換成 utf-16 編碼。現在我們來著重分析這個過程,AllocFromModifiedUtf8 對於存在非 ASCII 編碼的字元會執行到下面的一個 for 迴圈中,在迴圈中分別執行了 GetUtf16FromUtf8GetLeadingUtf16CharGetTrailingUtf16Char 函式,這三個函式在 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 函式首先判斷字元是幾個位元組編碼,如果是四位元組編碼需要特殊處理,轉換成代理對(surrogate pair);
GetTrailingUtf16CharGetLeadingUtf16Char 邏輯就很簡單了,獲取返回字串的低兩位位元組和高兩位位元組,如果高兩位位元組不為空就組合成一個四位元組 utf-16 編碼的字元並返回。所以最後得出的結論就是:AllocFromModifiedUtf8 函式返回的結果要麼全是 ASCII 字元的 utf-8 編碼字串,要麼就是 utf-16 編碼的字串。

分析到此處,我們可以知道 Android 8.0 及以上版本,在 Native 層建立 String 物件時,如果內容全部為 ASCII 字元,String 就是 utf-8 編碼,否則為 utf-16 編碼。那麼通過 Java 層建立的 String 物件呢?其實和從 Native 層建立的 String 物件情況一致,接下來我們會驗證。

五、 推論驗證

上面我們提出了兩個推論:

  1. Dalvik 中,String 物件編碼方式為 utf-16 編碼;
  2. ART 中,String 物件編碼方式為 utf-16 編碼,但是有一個情況除外:如果 String 物件全部為 ASCII 字元並且 Android 系統為 8.0 及之上版本,String 物件的編碼則為 utf-8;

為了驗證上面的推論,我們用兩種方式來論證:

5.1、 獲取 String 物件中字元佔用位元組數

首先想到最直接的方式就是在 Android 4.3 的手機上獲取一個 String 字串的佔用位元組數,測試程式碼如下所示:

String str = "hello from jni中文";
byte[] bytes = str.getBytes();
複製程式碼

最後觀察一下 byte[] 陣列的大小,最後發現是 20,並不是 32,也就是說該字串是 utf-8 編碼,並不是 utf-16 編碼,和之前得出的結論不一致;我們同樣在 Android 6.0 手機上執行相同的程式碼,發現大小同樣是 20。具體什麼原因呢,我們來看一下 getBytes 原始碼(分別在 String.javaCharset.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;
    }
}
複製程式碼

通過原始碼已經可以清晰的看到使用 getBytes 函式獲取的是 utf-8 編碼的字串。那麼我們怎麼知曉 Java 層 String 真正的編碼格式呢,可不可以直接檢視物件的記憶體佔用?我們來試一下,通過 Android Profiler 的 Dump Java Heap 功能我們可以清楚的看到一個物件佔用的記憶體,首先通過 String str = "hello from jni中文" 程式碼簡單的建立一個 String 物件,然後通過 Android Profiler 工具檢視這個物件的記憶體佔用,切換到 App HeapArrange by callstack,找到建立的 String 物件:

Android Profiler.png
Android Profiler.png

可以看到物件佔用大小是 48 個位元組,其中 char 陣列佔用的位元組是 32,每個字元都是佔用兩位元組,這個行為在 Android 8.0 之前的版本一致,所以我們可以很明確地推斷在 Android 8.0 之前通過上述方式建立的 String 物件都是 utf-16 編碼。

另外我們同時驗證一下在 Android 8.0 版本及以上全為 ASCII 字元的 String 物件記憶體佔用詳細情況,測試程式碼為 String output = "hello from jni"

Android_8.0_Profiler.png

可以看到佔用位元組數是 14,也就是單位元組的 utf-8 編碼,所以我們的推論 2 也成立。

上面分析完通過 String str = "hello from jni中文" 方式建立的 String 物件是 utf-16 編碼,另外,String 物件還有一種建立方式:通過 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));
    }
}
複製程式碼

通過程式碼我們可以知道,因為 char 為雙位元組,high 對應的是高位位元組,(data[offset++] & 0xff) 則為低位位元組,所以我們可以得出結論,String 物件通過這種情況下建立的同樣是 utf-16 編碼。

5.2、 官方資料

通過 5.1 小節的分析,我們已經可以通過實際表現來支撐我們上面的兩點推論,作為補充,我們同時查閱相關官方資料來對這些推論得到更加全面的認識:

一、 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).

我們重點看這一句

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 類是實現了 CharSequence 介面,所以自然而然是 utf-16 編碼;

二、Java HotSpot VM Options

-XX:+UseCompressedStrings
Use a byte[] for Strings which can be represented as pure ASCII. (Introduced in Java 6 Update 21 Performance Release)

這個選項就是和上面的 kUseStringCompression 變數對應。

六. 最後結論

經過上面的分析我們可以得出以下結論:

  1. Dalvik 中 String 物件編碼方式為 utf-16 編碼;
  2. ART 中 String 物件編碼方式為 utf-16 編碼,但是有一個情況例外:如果 String 物件全部為 ASCII 字元並且 Android 系統為 8.0 及之上,String 物件的編碼則為 utf-8;
  3. Android dalvik 中 utf-8 編碼轉 utf-16 編碼的函式有缺陷,沒有對 4 位元組的 utf-8 編碼做特殊處理,直到 ART 中才對該缺陷進行了修復。

6.1、 結論 3 驗證

結論 3 就回答了我們最早的那個疑問,這個結論需要做一個簡單的比較分析。我們回到最上面的問題:為什麼不直接使用 env->NewStringUTF() 函式進行轉換,而需要額外寫一個 UTF82UnicodeOne 函式。其實細心的人可能已經注意到了,上面 dalvik 和 ART 原始碼中 utf-8 到 utf-16 轉換函式是有區別的,我們把關鍵程式碼放到一起來進行對比:

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;
}
複製程式碼

發現了麼?dalvik 程式碼中並沒有對 4 位元組 utf-8 編碼的字串進行處理,而 ART 中專門用了很詳細的註釋說明了針對 4 位元組編碼的 utf-8 需要轉成代理對(surrogate pair)!為什麼之前 Android 版本沒有針對 4 位元組編碼進行處理?我的一個推測是:可能老版本的 Android 系統使用的是 ucs-2 編碼,並沒有對 BMP 之外的平面集做處理,所以也不存在 4 位元組的 utf-8,在擴充套件為 utf-16 編碼之後,自然而然就需要額外對 4 位元組的 utf-8 進行轉換成代理對的操作。

測試這個結論也很簡單,比如 “?” 是 4 位元組 utf-8 編碼字元(“?” 的 utf-8 編碼為 F0A0B296,線上查詢網站:Unicode和UTF編碼轉換),在 Android 4.3 上通過 env->NewStringUTF 的方式轉換之後會出現崩潰,在 Android 6.0 上則可以正常轉換並且交給 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 函式,就不會執行崩潰了。雖然不會崩潰,但是將轉換之後的 String 物件交給 Java 層卻顯示成亂碼,這是因為 stringTojstring 函式中並沒有針對 4 位元組編碼的 utf-8 字元轉換成代理對,解決辦法可以參考 ART 的 GetUtf16FromUtf8 函式,感興趣的讀者可以自己實踐一下。

經過上面的測試,我們做一個推測,UTF82UnicodeOne 函式的作者發現了上面我們描述的行為差異或者因為這個差異所引發的一些問題,才自己專門寫了這個 stringTojstring 函式做轉換,針對 4 位元組(5 位元組和 6 位元組的處理多餘)編碼的 utf-8 進行了單獨處理。

七、引用

JavaScript 的內部字元編碼是 UCS-2 還是 UTF-16
Dalvik虛擬機器中NewStringUTF的實現

相關文章