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)
參考:
Verifying App Behavior on the Android Runtime (ART)
Will my Android App still run with ART instead of Dalvik?
