天天看點

so庫你應該知道的基礎知識

LLDB全稱Low Level Debugger ,并不是低水準的調試器,而是輕量級的高性能調試器

每個作業系統都會為運作在該系統下的應用程式提供應用程式二進制接口(Application Binary Interface,ABI)

1. 什麼是CPU架構及ABI

Android系統目前支援以下七種不同的CPU架構:ARMv5,ARMv7 (從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。

  1. 為什麼需要重點關注.so檔案

如果項目中使用到了NDK,它将會生成.so檔案,是以顯然你已經在關注它了。如果隻是使用Java語言進行編碼,你可能在想不需要關注.so檔案了吧,因為Java是跨平台的。但事實上,即使你在項目中隻是使用Java語言,很多情況下,你可能并沒有意識到項目中依賴的函數庫或者引擎庫裡面已經嵌入了.so檔案,并依賴于不同的ABI。

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目錄中。

\

  1. 安裝Apk時PackageManagerService選擇解壓so檔案的政策

在Android系統中,當我們安裝Apk檔案的時候,lib目錄下的so檔案會被解壓App的原生庫目錄,一般來說是放到/data/data/package-name/lib目錄下,而根據系統和CPU架構的不同,其拷貝政策也是不一樣的,不正确地配置so檔案,比如某些App使用第三方的so時,隻配置了其中某一種CPU架構的so,可能會造成App在某些機型上的适配問題。

Android版本    so拷貝政策    政策問題

< 4.0    周遊Apk中檔案,當Apk中lib目錄下主abi子目錄中有so檔案存在時,則全部拷貝主abi子目錄下的so;

隻有當主abi子目錄下沒有so檔案的時候才會拷貝次abi子目錄下的so檔案。    當so放置不當時,安裝Apk時會導緻拷貝不全。比如Apk的lib目錄下存在armeabi/libx.so,armeabi/liby.so,armeabi-v7a/libx.so這3個so檔案,那麼在主abi為armeabi-v7a且系統版本小于4.0的手機上,Apk安裝後,按照拷貝政策,隻會拷貝主abi目錄下的檔案即armeabi-v7a/libx.so,當加載liby.so時就會報找不到so的異常。另外如果主abi目錄不存在,這個政策會周遊2次Apk,效率偏低。

4.0-4.0.3    周遊Apk中所有檔案,如果符合so檔案的規則,且為主abi目錄或者次abi目錄下的so,就解壓拷貝到相應目錄。    存在同名so覆寫,比如一個App的armeabi和armeabi-v7a目錄下都包含同名的so,那麼就會發生覆寫現象,覆寫的先後順序根據so檔案對應ZipFileR0中的hash值而定,考慮這樣一個例子,假設一個Apk同時有armeabi/libx.so和armeabi-v7a/libx.so,安裝到主abi為armeabi-v7a的手機上,拷貝so時根據周遊順序,存在一種可能即armeab-v7a/libx.so優先周遊并被拷貝,随後armeabi/libx.so被周遊拷貝,覆寫了前者。本來應該加載armeabi-v7a目錄下的so,結果按照這個政策拷貝了armeabi目錄下的so。\

4.0.4    周遊Apk中檔案,當周遊到有主abi目錄的so時,拷貝并設定标記hasPrimaryAbi為真,以後周遊則隻拷貝主abi目錄下的so,當标記為假的時候,如果周遊的so的entry名包含次abi字元串,則拷貝該so。    經過實際測試,so放置不當時,安裝Apk時存在so拷貝不全的情況。這個政策想解決的問題是在4.0~4.0.3系統中的so随意覆寫的問題,即如果有主abi目錄的so則拷貝,如果主abi目錄不存在這個so則拷貝次abi目錄的so,但代碼邏輯是根據ZipFileR0的周遊順序來決定是否拷貝so,假設存在這樣的Apk,lib目錄下存在armeabi/libx.so, armeabi/liby.so, armeabi-v7a/libx.so這三個so檔案,且hash的順序為armeabi-v7a/libx.so在armeabi/liby.so之前,則Apk安裝的時候liby.so根本不會被拷貝,因為按照拷貝政策,armeabi-v7a/libx.so會優先周遊到,由于它是主abi目錄的so檔案,是以标記被設定了,當周遊到armeabi/liby.so時,由于标記被設定為真,liby.so的拷貝就被忽略了,進而在加載liby.so的時候會報異常。

64位    分别處理32位和64位abi目錄的so拷貝,abi由周遊Apk結果的所有so中符合bilist清單的最靠前的序号決定,然後拷貝該abi目錄下的so檔案。    政策假定每個abi目錄下的so都放置完全的,這是和4.0以前版本一樣的處理邏輯,存在遺漏拷貝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.accessH.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解決。