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

Android Native 開發之 NewString 與 NewStringUtf 解析

本文將從一個 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 != '\0')
        *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

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

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 編碼;

-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 進行了單獨處理。

七、引用

相關推薦

Android Native 開發 NewString NewStringUtf 解析

本文將從一個 Native Crash 分析入手,帶大家瞭解一下我們平時開發中常用容易忽略但是又很值得學習底層原始碼知識。 一、問題起因 最近在專案中遇到一個 native crash,引起 crash 的程式碼如下所示: jstring stri

Android NDK開發旅(6):JNI函式完全解析專案實戰

對於基本型別而言,JNI與Java之間的對映是一對一的,比如Java中的int型別直接對應於C/C++中的jint;而對引用型別的處理卻是不同的,JNI把Java中的物件當作一個C指標傳遞到本地函式中,這個指標指向JVM中的內部資料結構,而內部資料結構在記憶體

android 開發 ListView Adapter 應用實踐

在開發android中,ListView 的應用顯得非常頻繁,只要需要顯示列表展示的應用,可以說是必不可少,下面是記錄開發中應用到ListView與Adapter 使用的例項: ListView 所在頁面中的佈局(listview_item.xml): <?xml version="1.0"

Android底層開發耳機插拔音訊通道切換例項

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

android開發fragmentactivity之間相互跳轉

   Fragment的產生與介紹 Android執行在各種各樣的裝置中,有小螢幕的手機,超大屏的平板甚至電視。針對螢幕尺寸的差距,很多情況下,都是先針對手機開發一套App,然後拷貝一份,修改佈局以適應平板神馬超級大屏的。難道無法做到一個App可以同時適應手機和平板麼

Android開發“ListViewRecyclerView的對比”

在Android開發最火熱的時候ListView是最長使用的一種展示多item的控制元件,而在2018年的現在已經很少有人用ListView了,使用最多當數RecyclerView了。 下面總結一下兩者的區別: 兩者的用法區別 佈局效果 對空資料的處理 HeaderV

Android開發RadioGroupRadioButton控制元件使用

      RadioButton即單選按鈕,它在開發中提供了一種“多選一”的操作模式,是Android開發中常用的一種元件,例如在使用者註冊時,選擇性別時只能從“男”或者“女”中選擇一個。與Web開發不同的是,在Android中可以使用RadioGroup來定義單選按鈕元件

Android開發ServiceIntentService的區別使用場景

Service Service 是長期執行在後臺的應用程式元件。 Service 不是一個單獨的程序,它和應用程式在同一個程序中,Service 也不是一個執行緒,它和執行緒沒有任何關係,所以它不能直接處理耗時操作。如果直接把耗時操作放在 Service 的 onStartCommand() 中,

Android外掛化開發AMS應用程式(客戶端ActivityThread、Instrumentation、Activity)通訊模型分析

今天主要分析下ActivityManagerService(服務端) 與應用程式(客戶端)之間的通訊模型,在介紹這個通訊模型的基礎上,再    簡單介紹實現這個模型所需要資料型別。         本文所介紹內容基於android2.2版本。由於Android版本的不同

Android開發ActionBarDrawerLayout

ActionBar位於Activity的頂部,可用來顯示activity的標題、Icon、Actions和一些用於互動的View。它也可被用於應用的導航。 ActionBar 是在Android 3.0(API 11)中加入到SK中的,想在低版本中使用Acti

Android混合開發Activity類html頁面之間的相互跳轉(並解決黑屏問題)

在底部有本程式原始碼下載 本程式流程:程式啟動-->testActivity--->phonegap2框架類--->index.html--->testActivity,主要實現activity與html頁面的相互跳轉,並實現 傳遞引數的功能。 程式

Android 遊戲開發主角的移動地圖的平滑滾動(十五)

程式碼的實現方式       還是以人物向右移動為例,我們須要三個座標 一個是m_HeroPos 來儲存人物在地圖中的X座標  一個是 mScreenPos 來儲存人物在螢幕中的顯示座標 mMapPos 來儲存地圖在手機螢幕中的顯示座標,按鍵盤右鍵後人物在地圖中的座標加上8畫素(表示行走的步長),當人物的座標

Android系統開發七:新增Android Native Service方法

一、 Android Service 介紹 Android 的 Service 分為兩種: Android Service 和 Native Service 。 Android Service :又稱為 Java Service ,是實現在框架層( framework )裡

Android開發反射註解

反射 類型別Class的使用 類型別Class的例項獲取方式有一下三種 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Android直播開發旅(4):MP3編碼格式分析lame庫編譯封裝

轉載請宣告出處:http://blog.csdn.net/andrexpert/article/77683776 一、Mp3編碼格式分析       MP3,全稱MPEG Audio Layer3,是一種高效的計算機音訊編碼方案,它以較大的壓縮比(1:10至1:12)將音

android開發手機微控制器藍芽模組通訊

之前兩篇都是在說與手機的連線,連線方法,和主動配對連線,都是手機與手機的操作,做起來還是沒問題的,但是最終的目的是與微控制器的藍芽模組的通訊。 下面是到目前為止嘗試的與微控制器的通訊方法,沒有成功,但是從思路上來說沒有問題,最大的問題是與微控制器配對的時候,微控制器的藍芽

Android直播開發旅(2):深度解析H.264編碼原理

 (碼字不易,轉載請申明出處:http://blog.csdn.net/andrexpert/article/details/71774230 ) 前 言     在學習H.264編碼之前,我們先了解一下在視訊直播的過程中,如果Camera採集的YUV影象不做任何處理

Unity3D]Unity3D遊戲開發UnityAndroid互動呼叫研究

本文轉載自: http://blog.csdn.net/qinyuanpei/article/details/39348677    記得"仙劍之父“姚壯憲作為評委參加Unity亞洲區的比賽時曾經感慨道:"我學生時也是痴迷於自己不斷鑽研遊戲開發,從各種小遊戲和小工具做起,並

Unity3D遊戲開發UnityAndroid互動呼叫研究

各位朋友,大家好,我是秦元培,歡迎大家關注我的部落格,我的部落格地址是blog.csdn.net/qinyuanpei。在前一篇文章中,我們研究了Android平臺上Unity3D的手勢操作並在之前的基礎上實現了手勢旋轉、放縮等功能。今天呢,我們繼續來研究Unity在Android平臺上擴充套件的內容

React—Native開發 Could not connect to development server(Android)解決方法

寫在最前面:    本來,我是有一篇部落格 RN開發之BUG 總結(持續更新) 來專門總結自己在React-Native開發中遇到的各種BUG 以及其解決辦法的。但是,由於 Could not conn