今天早上公司有几个员工反映,小米手机升级到5.0.2系统后,公司APP不能运行
经过一番辛苦 排查后,最后定为在AttachCurrentThread报错,导致程序crash
报错为:
[art/runtime/thread.cc:568] Check failed: &stack_variable > reinterpret_cast<
经过各种谷歌百度,最终发现问题是由于“从 Android 4.4 开始,Google 开发者引进了新的 Android 运行环境 ART(意思就是 Android Runtime。Android 官方页面的介绍中,也将其称作新的虚拟机),以替代旧的 Dalvik VM”
在一篇文章中看到下面这段话:
“
ART 模式下随机崩溃。
Dalvik 对于 native 代码和 Java 代码提供各自的栈,默认 native 栈有 1MB、Java 栈有 32KB。而在 ART 模式下,提供统一的栈。按说,ART 线程栈的大小应该与 Dalvik 的一样。如果你显式设置栈的大小,你可能需要在 ART 模式下运行的 app 里重新访问这些值。
Java 的 Thread 类有一个构造函数 Thread(ThreadGroup group, Runnable runnable, String threadName, long stackSize) 提供栈大小参数的设置,如果运行中出现 StackOverflowError 错误,可能需要手动增大 stackSize 值了。
C/C++ 需要调用 POSIX thread 的函数 pthread_attr_setstack() 和 pthread_attr_setstacksize()。如果 pthread 栈太小, 调用 JNI AttachCurrentThread() 方法会打印如下 log:
F/art: art/runtime/thread.cc:435] Attempt to attach a thread with a too-small stack (16384 bytes)
”我使用AttachCurrentThread语句的线程内部定义了两个数组
char RecvDataBuff[1024000]; // 接收数据buffer
char RecvData[1024000]; //返回的数据内容
导致当前线程超过线程默认栈大小,从而导致crash
解决方法:将此两个数组定义为全局变量,问题解决
Android 5.0,代号 Lollipop,源码终于在2014年12月3日放出,国内一大批厂商跟进。最大的改变是默认使用 ART(Android Runtime) ,替换了之前的 Dalvik 虚拟机,提出了 Material Design 界面风格。之前发布的 app 可能需要作一些改动,暂时收集了一些问题,希望对大家有所帮助。
1. Intent/Service
在低于 Android 5.0 版本,程序运行正常。用户抱怨在新的 Android 5.0 设备上崩溃,我们还没有最新的设备,所以暂时用 Android 模拟器调试。
在输出的 log 中可以看到这样的记录:
E/AndroidRuntime(26479): java.lang.RuntimeException: Unable to start activity ComponentInfo{PACKAGE_NAME/.ACTIVITY_NAME}: java.lang.IllegalArgumentException: Service Intent must be explicit: Intent { act=com.bda.controller.IControllerService }
E/GameActivity(18333): Caused by: java.lang.IllegalArgumentException: Service Intent must be explicit: Intent { act=com.bda.controller.IControllerService }
E/GameActivity(18333): at android.app.ContextImpl.validateServiceIntent(ContextImpl.java:1982)
E/GameActivity(18333): at android.app.ContextImpl.startServiceCommon(ContextImpl.java:2020)
E/GameActivity(18333): at android.app.ContextImpl.startService(ContextImpl.java:1995)
E/GameActivity(18333): at android.content.ContextWrapper.startService(ContextWrapper.java:533)
E/GameActivity(18333): at com.bda.controller.a.d(Unknown Source)
通过查看堆栈崩溃信息,我们看到使用了第三方的 controller.jar 包导致错误。Controller 是在设备屏幕上模拟游戏手柄功能的包,下载最新的 Moga developers SDK ,下载了 controller-sdk-std-1.3.1.zip,2013 Feb 01 发布的,有点旧了。里面有 com.bda.controller.jar,没有源码。
尝试 zip 解压 controller.jar 文件,反编译 .class 文件 com/bda/controller/BaseController.class
想查看 bytecode,使用 javap -c BaseController.class
public final boolean init(); Code: 0: aload_0 1: getfield #113 // Field mIsBound:Z 4: ifne 48 7: new #193 // class android/content/Intent 10: dup 11: ldc #165 // class com/bda/controller/IControllerService 13: invokevirtual #195 // Method java/lang/Class.getName:()Ljava/lang/String; 16: invokespecial #201 // Method android/content/Intent."<init>":(Ljava/lang/String;)V 19: astore_1 20: aload_0 21: getfield #142 // Field mContext:Landroid/content/Context; 24: aload_1 25: invokevirtual #204 // Method android/content/Context.startService:(Landroid/content/Intent;)Landroid/content/ComponentName; 28: pop 29: aload_0 30: getfield #142 // Field mContext:Landroid/content/Context; 33: aload_1 34: aload_0 35: getfield #132 // Field mServiceConnection:Lcom/bda/controller/Controller$ServiceConnection; 38: iconst_1 39: invokevirtual #208 // Method android/content/Context.bindService:(Landroid/content/Intent;Landroid/content/ServiceConnection;I)Z 42: pop 43: aload_0 44: iconst_1 45: putfield #113 // Field mIsBound:Z 48: aload_0 49: getfield #113 // Field mIsBound:Z 52: ireturn
你当然想查看源代码,用反编译工具 jad,或者临时用网络在线版 Show My Code,这个网站可以查看 Zend Guard 加密过的 .php 文件、Java 的 .class 文件、Adobe Flash 的 .swf 文件、.NET 程序 .exe, .dll 或者 QR 二维码,可以收藏一下。
public final boolean init() { if(!mIsBound) { Intent intent = new Intent(com.bda.controller.IControllerService.getName()); mContext.startService(intent); mIsBound = mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); } return mIsBound; }
根据上面的错误和代码看出,这里需要使用显式的 Intent(通过 setComponent(ComponentName) 或者setClass(Context, Class) 设置了 Component 的 Intent),上面的一句需要改成 Intent intent = new Intent(mContext, IControllerService.class);
或者 Intent intent = new Intent("com.bda.controller.IControllerService").setPackage("com.bda.controller");
官方文档 也提到使用显式的 Intent 来 startService/bindService 以确保安全。
Caution: To ensure your app is secure, always use an explicit intent when starting a Service and do not declare intent filters for your services. Using an implicit intent to start a service is a security hazard because you cannot be certain what service will respond to the intent, and the user cannot see which service starts. Beginning with Android 5.0 (API level 21), the system throws an exception if you call bindService() with an implicit intent.
Note: When starting a Service, you should always specify the component name. Otherwise, you cannot be certain what service will respond to the intent, and the user cannot see which service starts.
很多第三方的库都暴露出这种问题,需要更新一下。我们也用了 Google 的 Analytics tracking 库 libGoogleAnalyticsV2.jar。
E/GameActivity( 1137): java.lang.RuntimeException: Unable to start activity ComponentInfo{PACKAGE_NAME/ACTIVITY_NAME}: java.lang.IllegalArgumentException: Service Intent must be explicit: Intent { act=com.google.android.gms.analytics.service.START (has extras) }
尝试更新了到 v3 (现在有 v4 了)解决问题,需要改动一些代码。这里有文档迁移需要修改什么,EasyTracker: v2.x to v3。
2. MD5 符号找不到了。
MD5_CTX context; MD5_Init(&context); const char* text = "Hello, world!"; MD5_Update(&context, text, sizeof(text)); MD5_Final(md5_result, &context);
崩溃的 log 如下
E/art(21678): dlopen("/data/app/PACKAGE_NAME/lib/arm/libsixguns.so", RTLD_LAZY) failed: dlopen failed: cannot locate symbol "MD5_Final" referenced by "libXYZ.so"...
E/GAME(21678): native code library failed to load.
E/GAME(21678): java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "MD5_Final" referenced by "libXYZ.so"...
这是因为 Google 修改了底层 bionic libc 库的实现 ,一些隐藏的 API 移除了。一些依赖这些函数的代码可能无法运行。
修改方案,可以自行导入 MD5 库,反正代码也简短。
或者添加 --whole-archive 静态链接 crypto 库。因为 OpenSSL 也提供了 MD5 的实现,可以借用。openssl/crypto/md32_common.h
为了在最终的 .so 库中包含这些定义,添加 ld 链接命令 -Wl,-whole-archive crypto -Wl,-no-whole-archive。
--whole-archive 将在链接中包含归档文件 .a 所有的 .o 文件,而不是只在需要时才搜索,用来转 .a 文件成 .so 文件。gcc 不识别这个命令,所以需要使用 -Wl,-whole-archive,用好后需要添加 -Wl,-no-whole-archive 结束,因为 gcc 会在链接中会添加自己的 .a,以免受影响。
3. ART 模式下随机崩溃。
Dalvik 对于 native 代码和 Java 代码提供各自的栈,默认 native 栈有 1MB、Java 栈有 32KB。而在 ART 模式下,提供统一的栈。按说,ART 线程栈的大小应该与 Dalvik 的一样。如果你显式设置栈的大小,你可能需要在 ART 模式下运行的 app 里重新访问这些值。
Java 的 Thread 类有一个构造函数 Thread(ThreadGroup group, Runnable runnable, String threadName, long stackSize) 提供栈大小参数的设置,如果运行中出现 StackOverflowError 错误,可能需要手动增大 stackSize 值了。
C/C++ 需要调用 POSIX thread 的函数 pthread_attr_setstack() 和 pthread_attr_setstacksize()。如果 pthread 栈太小, 调用 JNI AttachCurrentThread() 方法会打印如下 log:
F/art: art/runtime/thread.cc:435] Attempt to attach a thread with a too-small stack (16384 bytes)
使用 JNI 的时候,出于效率因素,可能需要缓存一些方法 FindClass/GetFieldID/GetMethodID 返回的 ID,千万不要缓存 JNIEnv*,也不要在应用程序的整个生命周期将 native 线程附加到 Java 线程。用 adb shell setprop debug.checkjni 1 命令可以调试一些 JNI 错误,它不会影响已经运行的应用程序。
ART 模式下的 JNI 可能会抛出一些 Dalvik 不会抛的错误,可以用 CheckJNI,也就是上面的命令行捕捉错误。比如,在 proguard 混淆代码的时候脱掉了一些 native 方法,运行时候会抛 NoSuchMethodError 异常。现在 GetFieldID() 和 GetStaticFieldID() 方法抛出 NoSuchMethodError 异常,而不是返回 null。
E/AndroidRuntime: FATAL EXCEPTION: main E/AndroidRuntime: java.lang.NoSuchMethodError: no static or non-static method "Lcom/foo/Bar;.native_frob(Ljava/lang/String;)I" E/AndroidRuntime: at java.lang.Runtime.nativeLoad(Native Method) E/AndroidRuntime: at java.lang.Runtime.doLoad(Runtime.java:421) E/AndroidRuntime: at java.lang.Runtime.loadLibrary(Runtime.java:362) E/AndroidRuntime: at java.lang.System.loadLibrary(System.java:526)
Colleen Culbertson (Intel) 于 2014 年 11 月 4 日 提交
推出 Android L 后,虚拟机编译器将迁移至 ART (Android 运行时),而且仅迁移至面向 64 位的 ART。 ART 与 Dalvik 之间相比如何,这对于应用代码而言又意味着什么?
ART 是 AOT(提前)编译器,这表示 dex2oat 在应用初始安装过程中运行一次。 Dalvik 是 JIT(实时)编译器,仅在调用时运行。 为了获得更长的应用安装时间,ART 仅在调用应用之后释放处理器。 此外,新的垃圾回收程序和内存分配程序将减少暂停的次数和时间,这表示 ART 可提供更出色的响应能力并减少能耗。 但是请注意,运行时内存占用空间较少同样意味着编译二进制需要更高的存储。 ART 将配合 ARM、x86 和 MIPS 硬件一起使用,并在运行浮点运算时显示出极大的改进。
代码有何变化? ART 向后兼容,可以使用 Dex (Dalvik 可执行代码)字节代码格式。 因此,多数应用可以直接运行(甚至性能更高)。
但是,您需要确认几项内容并采用一些可用的优化:
System.getProperty("java.vm.version") 返回的值。 ART 是 "2.0.0"
或更高。
如果使用 JNI 来运行 C/C++ 代码,请确保使用 CheckJNI (在清单中设置debuggable="true"
)。 参阅 使用 CheckJNI 调试 Android JNI (注:该设置是预发布的调试,不应在发布代码中设置为 true)。
获取最新版本的工具。 在安装时,ART 执行二进制码验证比 Dalvik 严格。 Android 构建工具生成的代码应该可以使用,但是一些后处理工具(尤其是模糊处理工具)可能会生成无效文件(能够在 Dalvik 中使用,但是不能在 ART 中使用)。
考虑是否删除一些异常检查(即不再需要的异常检查,因为 ART 可立刻查看整个代码,Dalvik 将需要一段时间)。
删除大部分
System.gc()
调用,尤其是用于减少 GC_FOR_ALLOC
类型时间或碎片的调用。
不要将指针保存到对象实例数据中,不要将修改的指针传递到 Release...ArrayElements()
压缩回收程序(已在 AOSP 中使用)可能会将对象迁移至内存, 将 Get
和 Release
调用迁移至ArrayElements()
可能会破坏内存。 如要对返回的阵列元素做出任何更改,必须调用合适的函数:
未更改阵列 > 使用 JNI_ABORT
模式(发布内存,无副本返回)。
做出了更改,但是无需参考 > 使用代码 0
(更新阵列对象,释放副本)。
做出了更改且需要遵循 > 使用 JNI_COMMIT
(这可更新底层阵列对象并保留副本)。
请勿尝试查看 Object
字段,因为它现在使用的是私有字段。 当将类层次迭代为序列化框架的一部分时,请在 Class.getSuperclass()==java.lang.Object.class
时停止(返回空值时停止)。
在 ART 中使用其他错误处理和日志
改进了 从 GetMethodID() 中的 NoSuchMethodError : 以及从 RegisterNatives 调用 中的 GetStaticMethodID() 的 throw 和日志
指令 (可能由于 ProGuard 等工具将方法删除导致)
A
NoSuchFieldError (
而非空值(来自 GetFieldID()
和 GetStaticFieldID()
当子类尝试覆盖隐私数据包方法时将出现一个警告。如要覆盖类方法,请将该方法声明为 public 或 受保护。
使用 ART 验证程序进行标记的其他问题包括:
控制流无效
moniterenter/monitorexit 不平衡
0 长度类型的参数列出尺寸
请注意,JNI 规格策略更严格,包括 CallNonvirtual---Method() 方法 需要声明类,而非子类的方法。
请观察 ART 统一的线程堆栈的尺寸,该尺寸应相当于两个 Dalvik 堆栈的尺寸(默认 32 KB Java 堆栈和 1 MB 本地堆栈)。 需要检查堆栈尺寸明确设置的位置 — 包括
Thread
构建程序的 Java 调用,以及当发生 StackOverflowError
时尺寸增长。
观察 pthread 尺寸(pthreat_attr_setstack() 和 pthreat_attr_setstacksize()
)
,因为包括 AttachCurrentThread() 的调用将会 throw 一个错误。
删除现有
.odex
文件格式的依赖性(在 /system/framework
、/data/dalvik-cache
或 DexClassLoader
优化的输出目录中)。 虽然 ART 尽量与 ELF 采用相同的命名和锁定规则,但是应用不应依赖文件格式。
使用 Mockito,让 Proxy
InvocationHandler.invoke()
正确接收空值(
而非空阵列) (如果没有增量)。
查看所有收到的通知。 Android L 中采用了新的颜色方案。
使用 android.app.Norification.Builder.setColor() 在图示
图像后的圆圈中设置重点色。
请记住,仅可在主要通知图示和操作图示上使用仅限 alpha 的通道
查看警告通知(即使用 fullScreenIntent
的通知,或使用 ringtone/vibrations 的高权限通知)
参考文献:
Android 应用保留 ART,还是更换为 Dalvik?