本文將從一個 Native Crash 分析入手,帶你們瞭解一下咱們平時開發中經常使用容易忽略可是又很值得學習底層源碼知識。php
最近在項目中遇到一個 native crash,引發 crash 的代碼以下所示:html
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)
這行代碼拋出異常。java
這個 crash 最後的解決方法是及時調用 env->ExceptionClear
清除這個異常便可。回頭詳細分析這個函數,新的疑惑就出現了,爲何會存在這麼一個轉換函數,咱們知道將 c++ 裏面的 string 類型轉成 jni 層的 jstring 類型有一個更加簡便的函數 env->NewStringUTF(str.c_str())
,爲何不直接調用這個函數,而須要經過這麼複雜的步驟進行 string 到 jstring 的轉換,接下來咱們會仔細分析相關源碼來解答這個疑惑。先把相關的幾個函數源碼貼出來:android
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;
}
複製代碼
因爲沒法找到代碼的出處和做者,因此如今咱們只能經過源碼去推測意圖。c++
首先咱們先看第一個函數 UTF82Unicode
,這個函數顧名思義是將 utf-8 編碼轉成 unicode(utf-16) 編碼。而後分析第二個函數 UTF82UnicodeOne
,這個函數看起來會比較費解,由於這涉及到 utf-16 與 utf-8 編碼轉換的知識,因此咱們先來詳細瞭解一下這兩種經常使用編碼。git
首先須要明確的一點是咱們平時說的 unicode 編碼其實指的是 ucs-2 或者 utf-16 編碼,unicode 真正是一個業界標準,它對世界上大部分的文字系統進行了整理、編碼,它只規定了符號的二進制代碼,卻沒有規定這個二進制代碼應該如何存儲。因此嚴格意義上講 utf-八、utf-16 和 ucs-2 編碼都是 unicode 字符集的一種實現方式,只不過前二者是變長編碼,後者則是定長。segmentfault
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 編碼反而會浪費空間。bash
除了上面介紹到的幾種編碼方式,還有 utf-32 編碼,也被稱爲 ucs-4 編碼,它對於每一個字符統一使用 4 個字節來表示。須要注意的是,utf-16 編碼是 ucs-2 編碼的擴展(在 unicode 引入字符平面集概念以前,他們是同樣的),ucs-2 編碼在基本多文種平面字符集上和 utf-16 結果一致,可是 utf-16 編碼可使用 4 個字節來表示基本多文種平面以外的字符集,前兩個字節稱爲前導代理,後兩個字節稱爲後尾代理,這兩個代理構成一個代理對。unicode 總共有 17 個字符平面集:oracle
平面 | 始末字符值 | 中文名稱 | 英文名稱 |
---|---|---|---|
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 編碼之間的轉換公式,後面咱們立刻就會用到。
咱們回到上面的問題:爲何不直接使用 env->NewStringUTF
,而是須要先作一個 utf-8 編碼到 utf-16 編碼的轉換,將轉換以後的值經過 env->NewString
生成一個 jstring 呢?應該能夠肯定是做者有意爲之,因而咱們下沉到源碼中去尋找問題的答案。
由於 dalvik 和 ART 的行爲表現是有差別的,因此咱們有必要來了解一下二者的實現:
首先咱們來分析一下 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 != '\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",轉換步驟以下:
one = *(*pUtf8Ptr)++;
將入參 char** pUtf8Ptr
解引用,獲取字符串指針,再解一次,並將指針後移,其實就是獲取字符表明的 'a'(0x61),而後 0x61&0x80 = 0x00,說明這是單字節的 utf-8 字符,返回 0x61 給上層,因爲上層是 u2(typedef uint16_t u2),因此上層將結果存儲爲 0x000x61;((one & 0x0f) << 12) | ((two & 0x3f) << 6) | (three & 0x3f);
,這個語句對應的就是 utf-8 與 utf-16 的轉換公式,最後返回結果是 0x4E2D,這個也是 「中」 的 unicode 字符集,返回給外層存儲爲 0x4E2D;回顧整個過程咱們能夠發現,NewString
和 NewStringUTF
生成的 jstring 對象都是 utf-16 編碼,因此這裏咱們能夠得出一個推論:在 dalvik 虛擬機中,native 方法建立的 String 對象都是 utf-16 編碼。那麼 Java 類中建立的 String 對象是什麼編碼呢?其實也是 utf-16,後面咱們會證明這個推論。
分析完 dalvik 源碼以後,咱們來分析一下 ART 的相關源碼(這裏選取的是 Android 8.0 源碼),一樣的流程,先是兩個函數的調用時序圖:
這兩個函數實如今 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);
}
複製代碼
能夠看到他們調用的函數分別是 AllocFromUtf16
和 AllocFromModifiedUtf8
,這兩個函數在 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
函數在 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
函數。AllocFromUtf16
和 ConvertModifiedUtf8ToUtf16
這兩個函數中都有對 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 循環中,在循環中分別執行了 GetUtf16FromUtf8
、GetLeadingUtf16Char
和 GetTrailingUtf16Char
函數,這三個函數在 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); GetTrailingUtf16Char
和 GetLeadingUtf16Char
邏輯就很簡單了,獲取返回字符串的低兩位字節和高兩位字節,若是高兩位字節不爲空就組合成一個四字節 utf-16 編碼的字符並返回。因此最後得出的結論就是:AllocFromModifiedUtf8
函數返回的結果要麼全是 ASCII 字符的 utf-8 編碼字符串,要麼就是 utf-16 編碼的字符串。
分析到此處,咱們能夠知道 Android 8.0 及以上版本,在 Native 層建立 String 對象時,若是內容所有爲 ASCII 字符,String 就是 utf-8 編碼,不然爲 utf-16 編碼。那麼經過 Java 層建立的 String 對象呢?其實和從 Native 層建立的 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.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;
}
}
複製代碼
經過源碼已經能夠清晰的看到使用 getBytes 函數獲取的是 utf-8 編碼的字符串。那麼咱們怎麼知曉 Java 層 String 真正的編碼格式呢,可不能夠直接查看對象的內存佔用?咱們來試一下,經過 Android Profiler 的 Dump Java Heap 功能咱們能夠清楚的看到一個對象佔用的內存,首先經過 String str = "hello from jni中文"
代碼簡單的建立一個 String 對象,而後經過 Android Profiler 工具查看這個對象的內存佔用,切換到 App Heap
與 Arrange by callstack
,找到建立的 String 對象:
能夠看到對象佔用大小是 48 個字節,其中 char 數組佔用的字節是 32,每一個字符都是佔用兩字節,這個行爲在 Android 8.0 以前的版本一致,因此咱們能夠很明確地推斷在 Android 8.0 以前經過上述方式建立的 String 對象都是 utf-16 編碼。
另外咱們同時驗證一下在 Android 8.0 版本及以上全爲 ASCII 字符的 String 對象內存佔用詳細狀況,測試代碼爲 String output = "hello from jni"
:
能夠看到佔用字節數是 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.1 小節的分析,咱們已經能夠經過實際表現來支撐咱們上面的兩點推論,做爲補充,咱們同時查閱相關官方資料來對這些推論獲得更加全面的認識:
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 編碼;
-XX:+UseCompressedStrings Use a byte[] for Strings which can be represented as pure ASCII. (Introduced in Java 6 Update 21 Performance Release)
這個選項就是和上面的 kUseStringCompression
變量對應。
通過上面的分析咱們能夠得出如下結論:
結論 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的實現