在安卓項目里部署so文件你需要知道的知識
1. 什么是CPU架構及ABI
Android系統目前支持以下七種不同的CPU架構:ARMv***RMv7 (從2010年起),x86 (從2011年起),MIPS (從2012年起),ARMv8,MIPS64和x86_64 (從2014年起),每一種都關聯著一個相應的ABI。
應用程序二進制接口(Application Binary Interface)定義了二進制文件(尤其是.so文件)如何運行在相應的系統平臺上,從使用的指令集、內存對齊到可用的系統函數庫。在Android系統上,每一個CPU架構對應一個ABI:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。
2. 為什么需要重點關注.so文件
如果項目中使用到了NDK,它將會生成.so文件,因此顯然你已經在關注它了。如果只是使用Java語言進行編碼,你可能在想不需要關注.so文件了吧,因為Java是跨平臺的。但事實上,即使你在項目中只是使用Java語言,很多情況下,你可能并沒有意識到項目中依賴的函數庫或者引擎庫里面已經嵌入了.so文件,并依賴于不同的ABI。例如,項目中使用RenderScript支持庫,OpenCV,Unity,android-gif-drawable,SQLCipher等,你都已經在生成的APK文件中包含.so文件了,而你需要關注.so文件。
Android應用支持的ABI取決于APK中位于lib/ABI目錄中的.so文件,其中ABI可能是上面說過的七種ABI中的一種。
Native Libs Monitor這個應用可以幫助我們理解手機上安裝的APK用到了哪些.so文件,以及.so文件來源于哪些函數庫或者框架。當然,我們也可以自己對APP反編譯來獲取這些信息,不過相對麻煩一些。
很多設備都支持多于一種的ABI,例如ARM64和x86設備也可以同時運行armeabi-v7a和armeabi的二進制包。但***是針對特定平臺提供相應平臺的二進制包,這種情況下運行時就少了一個模擬層(例如x86設備上模擬arm的虛擬層),從而得到更好的性能(歸功于最近的架構更新,例如硬件fpu,更多的寄存器,更好的向量化等)。我們可以通過Build.SUPPORTED_ABIS得到根據偏好排序的設備支持的ABI列表。但你不應該從你的應用程序中讀取它,因為Android包管理器安裝APK時,會自動選擇APK包中為對應系統ABI預編譯好的.so文件,如果在對應的lib/ABI目錄中存在.so文件的話。
3. .so文件應該放在什么地方
我們往往很容易對.so文件應該放在或者生成到哪里感到困惑,下面是一個總結:
- Android Studio工程放在main/jniLibs/ABI目錄中(當然也可以通過在build.gradle文件中的設置jniLibs.srcDir屬性自己指定)
- Eclipse工程放在libs/ABI目錄中(這也是ndk-build命令默認生成.so文件的目錄)
- AAR壓縮包中位于jni/ABI目錄中(.so文件會自動包含到引用AAR壓縮包的APK中)
- 最終APK文件中的lib/ABI目錄中
- 通過PackageManager安裝后,在小于Android 5.0的系統中,.so文件位于app的nativeLibraryPath目錄中;在大于等于Android 5.0的系統中,.so文件位于app的nativeLibraryRootDir/CPU_ARCH目錄中。
4. 安裝Apk時PackageManagerService選擇解壓so文件的策略
在Android系統中,當我們安裝Apk文件的時候,lib目錄下的so文件會被解壓App的原生庫目錄,一般來說是放到/data/data/package-name/lib目錄下,而根據系統和CPU架構的不同,其拷貝策略也是不一樣的,不正確地配置so文件,比如某些App使用第三方的so時,只配置了其中某一種CPU架構的so,可能會造成App在某些機型上的適配問題。
Android版本
so拷貝策略
策略問題
5. 配置so的建議
針對Android 系統的這些拷貝策略的問題,我們給出了一些配置so的建議:
5.1 針對armeabi和armeabi-v7a兩種ABI
- 方法1:由于armeabi-v7a指令集兼容armeabi指令集,所以如果損失一些應用的性能是可以接受的,同時不希望保留庫的兩份拷貝,可以移除armeabi-v7a目錄和其下的庫文件,只保留armeabi目錄;比如Apk使用第三方的so只有armeabi這一種ABI時,可以考慮去掉Apk中lib目錄下armeabi-v7a目錄。
- 方法2:在armeabi和armeabi-v7a目錄下各放入一份so。
5.2 針對x86
目前市面上的x86機型,為了兼容arm指令,基本都內置libhoudini模塊,即二進制轉碼支持,該模塊負責把ARM指令轉換為x86指令,所以如果是出于Apk包大小的考慮,并且可以接受一些性能損失,可以選擇刪掉x86庫目錄,x86下配置的armeabi目錄的so庫一樣可以正常加載使用。
5.3 針對64位ABI
如果App開發者打算支持64位,那么64位的so要放全,否則可以選擇不單獨編譯64位的so,全部使用32位的so,64位機型默認支持32位so的加載。比如Apk使用第三方的so只有32位ABI的so,可以考慮去掉Apk中lib目錄下的64位ABI子目錄,保證Apk安裝后正常使用。
5. Android Studio配置abiFilters
- android {
- defaultConfig {
- ndk {
- abiFilters 'armeabi-v7a' //, 'armeabi', 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64'
- }
- }
- }
這句話的意思就是指定NDK需要兼容的架構,把除了armeabi-v7a以外的兼容包都過濾掉,只剩下一個armeabi-v7a的文件夾。
即使我們沒有指定其他的兼容框架,也需要一個過濾。當我們接入多個第三方庫時,很可能第三方庫做了多個平臺的兼容。譬如fresco就做了各個平臺的兼容,所以它創建了各個兼容平臺的目錄。因為只要出現了這個目錄,系統就只會在這個目錄里找.so文件而不會遍歷其他的目錄,所以就出現了找不到.so文件的情況。
6. java.lang.UnsatisfiedLinkError
該錯誤類型較多,以下進行分類:
- java.lang.UnsatisfiedLinkError : dlopen failed: library //dlopen打開失敗
- java.lang.UnsatisfiedLinkError :findLibrary returned null //找不到library
- java.lang.UnsatisfiedLinkError : Native method not found //找不到對應函數
- java.lang.UnsatisfiedLinkError :Cannot load library: load_library //無法load library
出現原因:
顯然出現上述崩潰的根本原因是:
- (1)so無法加載,可能是so不存在等原因
- (2)so正常加載,但是沒有找到相應的函數
針對第二個原因,顯然相對來說很容易排查,而且在開發中,這樣的函數調用必然會在編譯時和debug模式下進行測試,所以這種原因產生的概率很小。
那么下面主要總結幾類“so無法加載”而導致上述崩潰的幾種原因:
6.1 生成的so本身缺陷
一個簡單的例子:
crash堆棧:
- java.lang.UnsatisfiedLinkError: Cannot load library: find_library(linker.cpp:889): "/data/data/com.netease.nis.apptestunit/app_lib/libdemo.so" failed to load previously
- at java.lang.Runtime.load(Runtime.java:340)
- at java.lang.System.load(System.java:521)
- at com.netease.nis.bugrpt.ReLinker.loadLibrary(ReLinker.java:76)
- at com.example.crash.MainActivity.onCreate(MainActivity.java:272)
- at android.app.Activity.performCreate(Activity.java:5220)
- at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1086)
- at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2193)
- at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2279)
- at android.app.ActivityThread.access$600(ActivityThread.java:142)
- at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1272)
- at android.os.Handler.dispatchMessage(Handler.java:99)
- at android.os.Looper.loop(Looper.java:137)
- at android.app.ActivityThread.main(ActivityThread.java:5105)
- at java.lang.reflect.Method.invokeNative(Native Method)
- at java.lang.reflect.Method.invoke(Method.java:511)
- at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
- at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
- at dalvik.system.NativeStart.main(Native Method)
解決方法:
查看原項目Application.mk,發現APP_STL := gnustl_shared。原方案使用的是共享庫,這不一定都支持所有的機型,改用靜態庫gnustl_static問題解決。
對應的在Android Studio中需要將共享庫改用靜態庫gnustl_static。這一類關于so編譯共享庫問題,需要進行檢查。
- APP_STL 可用值
- system 系統默認
- stlport_static - 使用STLport作為靜態庫
- stlport_shared - 使用STLport 作為共享庫
- gnustl_static - 使用GNU libstdc++ 作為靜態庫
- gnustl_shared - 使用GNU libstdc++ 作為共享庫
上述例子只是一個簡單的例子,可能在so編譯生成時,由于沒有考慮共享庫的機型匹配等原因導致UnsatisfiedLinkError崩潰,其次是64位32位系統架構問題,也可能導致UnsatisfiedLinkError崩潰。
6.2 手機設備沒有空間
在so正確生成情況下,會根據設置的支持so庫框架生成對應的庫。在Android系統中,當我們安裝Apk文件的時候,lib目錄下的so文件會被解壓到App的原生庫目錄,一般來說是放到/data/data/package-name/lib目錄下,當準備加載native層的so時,雖然在Apk中有對應的so文件,但是由于手機設備沒有足夠的空間加載該so,導致加載失敗,產生上述崩潰。
6.3 so配置錯誤
倘若so正確生成,且手機空間充足,那么如上所述,在Android系統中,當我們安裝Apk文件的時候,lib目錄下的so文件會被解壓到App的原生庫目錄,一般來說是放到/data/data/package-name/lib目錄下。但是根據系統和CPU架構的不同,其拷貝策略也是不一樣的。倘若不正確地配置了so文件,比如某些App使用第三方的so時,只配置了其中某一種CPU架構的so,可能會造成App在某些機型上的適配問題,產生上述崩潰。
6.4 Android的PackageManager安裝問題
用戶安裝了與手機CPU架構不符的Apk安裝包,或者App升級過程中因各種原因未正確釋放so文件。這種問題可以使用ReLinker解決。
使用ReLinker十分簡單,使用
- ReLinker.loadLibrary(context, “mylibrary”)
代替標準的即可。
- System.loadLibrary(“mylibrary”);