时间: 2020-09-03 00:08:26 人气: 2349 评论: 0
字符串是软件开发中最为常见的对象之一,同时在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 编码转换的知识,所以我们先来详细了解一下这两种常用编码。
首先需要明确的一点是我们平时说的 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 编码之间的转换公式,后面我们马上就会用到。
我们回到上面的问题:为什么不直接使用 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 = 技术沙龙 教程文章 热点综合