ANDROID動態(tài)加載 使用SO庫時要注意的一些問題
基本信息
作者:kaedea
項目:android-dynamical-loading
Android項目里的SO庫
正好動態(tài)加載系列文章談到了加載SO庫的地方,我覺得這里可以順便談?wù)勈褂肧O庫時需要注意的一些問題?;蛟S這些問題對于經(jīng)常和SO庫開發(fā)打交道的同學(xué)來說已經(jīng)是老生長談,但是既然要討論一整個動態(tài)加載系列,我想還是有必要說說使用SO庫時的一些問題。
在項目里使用SO庫非常簡單,在 加載SD卡中的SO庫 中也有談到,只需要把需要用到的SO庫拷貝進 jniLibs(或者Eclipse項目里面的libs) 中,然后在JAVA代碼中調(diào)用 System.loadLibrary(“xxx”) 加載對應(yīng)的SO庫,就可以使用JNI語句調(diào)用SO庫里面的Native方法了。
但是有同學(xué)注意到了,SO庫文件可以隨便改文件名,卻不能任意修改文件夾路徑,而是“armeabi”、“armeabi-v7a”、“x86”等文件夾名有著嚴格的要求,這些文件夾名有什么意義么?
SO庫類型和CPU架構(gòu)類型
原因很簡單,不同CPU架構(gòu)的設(shè)備需要用不同類型SO庫(從文件名也可以猜出來個大概嘛 ╮( ̄▽ ̄”)╭)。
記得還在學(xué)校的時候,提及ARM處理器時,老師說以后移動設(shè)備的CPU基本就是ARM類型的了。老師不曾欺我,早期的Android系統(tǒng)幾乎只支持ARM的CPU架構(gòu),不過現(xiàn)在至少支持以下七種不同的CPU架構(gòu):ARMv5,ARMv7,x86,MIPS,ARMv8,MIPS64和x86_64。每一種CPU類型都對應(yīng)一種ABI(Application Binary Interface),“armeabi-v7a”文件夾前面的“armeabi”指的就是ARM這種類型的ABI,后面的“v7a”指的是ARMv7。這7種CPU類型對應(yīng)的SO庫的文件夾名是:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。
不同類型的移動設(shè)備在運行APP時,需要加載自己支持的類型的SO庫,不然就GG了。通過 Build.SUPPORTED_ABIS 我們可以判斷當(dāng)前設(shè)備支持的ABI,不過一般情況下,不需要開發(fā)者自己去判斷ABI,Android系統(tǒng)在安裝APK的時候,不會安裝APK里面全部的SO庫文件,而是會根據(jù)當(dāng)前CPU類型支持的ABI,從APK里面拷貝最合適的SO庫,并保存在APP的內(nèi)部存儲路徑的 libs 下面。(這里說一般情況,是因為有例外的情況存在,比如我們動態(tài)加載外部的SO庫的時候,就需要自己判斷ABI類型了。)
一種CPU架構(gòu) = 一種對應(yīng)的ABI參數(shù) = 一種對應(yīng)類型的SO庫
到這里,我們發(fā)現(xiàn)使用SO庫的邏輯還是比較簡單的,但是Android系統(tǒng)加載SO庫的邏輯還是給我們留下了一些坑。
使用SO庫時要注意的一些問題
1. 別把SO庫放錯地方
SO庫其實都是APP運行時加載的,也就是說APP只有在運行的時候才知道SO庫文件的存在,這就無法通過靜態(tài)代碼檢查或者在編譯APP時檢查SO庫文件是否正常。所以,Android開發(fā)對SO庫的存放路徑有嚴格的要求。
使用SO庫的時候,除了“armeabi-v7a”等文件夾名需要嚴格按照規(guī)定的來自外,SO庫要放在項目的哪個文件夾下也要按照套路來,以下是一些總結(jié):
- Android Studio 工程放在 jniLibs/xxxabi 目錄中(當(dāng)然也可以通過在build.gradle文件中的設(shè)置jniLibs.srcDir屬性自己指定);
- Eclipse 工程放在 libs/xxxabi 目錄中(這也是使用ndk-build命令生成SO庫的默認目錄);
- aar 依賴包中位于 jni/ABI 目錄中(SO庫會自動包含到引用AAR壓縮包到APK中);
- 最終構(gòu)建出來的APK文件中,SO庫存在 lib/xxxabi 目錄中(也就是說無論你用什么方式構(gòu)建,只要保證APK包里SO庫的這個路徑?jīng)]錯就沒問題);
- 通過 PackageManager 安裝后,在小于 Android 5.0 的系統(tǒng)中,SO庫位于 APP 的 nativeLibraryPath 目錄中;在大于等于 Android 5.0 的系統(tǒng)中,SO庫位于 APP 的 nativeLibraryRootDir/CPU_ARCH 目錄中;
既然扯到了這里,順便說一下,我在使用 Android Studio 1.5 構(gòu)建APK的時候,發(fā)現(xiàn) Gradle 插件只會默認打包application類型的module的jniLibs下面的SO庫文件,而不會打包aar依賴包的SO庫,所以會導(dǎo)致最終構(gòu)建出來的APK里的SO庫文件缺失。暫時的解決方案是把所有的SO庫都放在application模塊中(這顯然不是很好的解決方案),不知道這是不是Studio的BUG,同事的解決方案是通過修改Gradle插件來增加對aar依賴包的SO庫的打包支持(GitHub有開源的第三方Gradle插件項目,使用Java和Groovy語言開發(fā))。
2. 盡可能提供CPU支持的最優(yōu)SO庫
當(dāng)一個應(yīng)用安裝在設(shè)備上,只有該設(shè)備支持的CPU架構(gòu)對應(yīng)的SO庫會被安裝。但是,有時候,設(shè)備支持的SO庫類型不止一種,比如大多的X86設(shè)備除了支持X86類型的SO庫,還兼容ARM類型的SO庫(目前應(yīng)用市場上大部分的APP只適配了ARM類型的SO庫,X86類型的設(shè)備如果不能兼容ARM類型的SO庫的話,大概要嗝屁了吧)。
所以如果你的APK只適配了ARM類型的SO庫的話,還是能以兼容的模式在X86類型的設(shè)備上運行(比如華碩的平板),但是這不意味著你就不用適配X86類型的SO庫了,因為X86的CPU使用兼容模式運行ARM類型的SO庫會異??D(試著回想幾年前你開始學(xué)習(xí)Android開發(fā)的時候,在PC上使用AVD模擬器的那種感覺)。
3. 注意SO庫的編譯版本
除了要注意使用了正確CPU類型的SO庫,也要注意SO庫的編譯版本的問題。雖然現(xiàn)在的Android Studio支持在項目中直接編譯SO庫,但是更多的時候我們還是選擇使用事先編譯好的SO庫,這時就要注意了,編譯APK的時候,我們總是希望使用最新版本的build-tools來編譯,因為Android SDK最新版本會幫我們做出最優(yōu)的向下兼容工作。
但是這對于編譯SO庫來說就不一樣了,因為NDK平臺不是向下兼容的,而是向上兼容的。應(yīng)該使用app的minSdkVersion對應(yīng)的版本的NDK標(biāo)本來編譯SO庫文件,如果使用了太高版本的NDK,可能會導(dǎo)致APP性能低下,或者引發(fā)一些SO庫相關(guān)的運行時異常,比如“UnsatisfiedLinkError”,“dlopen: failed”以及其他類型的Crash。
一般情況下,我們都是使用編譯好的SO庫文件,所以當(dāng)你引入一個預(yù)編譯好的SO庫時,你需要檢查它被編譯所用的平臺版本。
4. 盡可能為每種CPU類型都提供對應(yīng)的SO庫
比如有時候,因為業(yè)務(wù)的需求,我們的APP不需要支持AMR64的設(shè)備,但這不意味著我們就不用編譯ARM64對應(yīng)的SO庫。舉個例子,我們的APP只支持armeabi-v7a和x86架構(gòu),然后我們的APP使用了一個第三方的Library,而這個Library提供了AMR64等更多類型CPU架構(gòu)的支持,構(gòu)建APK的時候,這些ARM64的SO庫依然會被打包進APK里面,也就是說我們自己的SO庫沒有對應(yīng)的ARM64的SO庫,而第三方的Library卻有。這時候,某些ARM64的設(shè)備安裝該APK的時候,發(fā)現(xiàn)我們的APK里帶有ARM64的SO庫,會誤以為我們的APP已經(jīng)做好了AMR64的適配工作,所以只會選擇安裝APK里面ARM64類型的SO庫,這樣會導(dǎo)致我們自己項目的SO庫沒有被正確安裝(雖然armeabi-v7a和x86類型的SO庫確實存在APK包里面)。
這時正確的做法是,給我們自己的SO庫也提供AMR64支持,或者不打包第三方Library項目的ARM64的SO庫。使用第二種方案時,可以把APK里面不需要支持的ABI文件夾給刪除,然后重新打包,而在Android Studio下,則可以通過以下的構(gòu)建方式指定需要類型的SO庫。
- productFlavors {
- flavor1 {
- ndk {
- abiFilters "armeabi-v7a"
- abiFilters "x86"
- abiFilters "armeabi"
- }
- }
- flavor2 {
- ndk {
- abiFilters "armeabi-v7a"
- abiFilters "x86"
- abiFilters "armeabi"
- abiFilters "arm64-v8a"
- abiFilters "x86_64"
- }
- }
- }
需要說明的是,如果我們的項目是SDK項目,我們最好提供全平臺類型的SO庫支持,因為APP能支持的設(shè)備CPU類型的數(shù)量,就是項目中所有SO庫支持的最少CPU類型的數(shù)量(使用我們SDK的APP能支持的CPU類型只能少于等于我們SDK支持的類型)。
5. 不要通過“減少其他CPU類型支持的SO庫”來減少APK的體積
確實,所有的x86/x86_64/armeabi-v7a/arm64-v8a設(shè)備都支持armeabi架構(gòu)的SO庫,因此似乎移除其他ABIs的SO庫是一個減少APK大小的好辦法。但事實上并不是,這不只影響到函數(shù)庫的性能和兼容性。
X86設(shè)備能夠很好的運行ARM類型函數(shù)庫,但并不保證100%不發(fā)生crash,特別是對舊設(shè)備,兼容只是一種保底方案。64位設(shè)備(arm64-v8a, x86_64, mips64)能夠運行32位的函數(shù)庫,但是以32位模式運行,在64位平臺上運行32位版本的ART和Android組件,將丟失專為64位優(yōu)化過的性能(ART,webview,media等等)。
過減少其他CPU類型支持的SO庫來減少APK的體積不是很明智的做法,如果真的需要通過減少SO庫來做APK瘦身,我們也有其他辦法。
減少SO庫體積的正確姿勢
1. 構(gòu)建特定ABI支持的APK
我們可以構(gòu)建一個APK,它支持所有的CPU類型。但是反過來,我們可以為每個CPU類型都單獨構(gòu)建一個APK,然后不同CPU類型的設(shè)備安裝對應(yīng)的APK即可,當(dāng)然前提是應(yīng)用市場得提供用戶設(shè)備CPU類型設(shè)別的支持,就目前來說,至少PLAY市場是支持的。
Gradle可以通過以下配置生成不同ABI支持的APK(引用自別的文章,沒實際使用過):
- android {
- ...
- splits {
- abi {
- enable true
- reset()
- include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
- universalApk true //generate an additional APK that contains all the ABIs
- }
- }
- // map for the version code
- project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9]
- android.applicationVariants.all { variant ->
- // assign different version code for each output
- variant.outputs.each { output ->
- output.versionCodeOverride =
- project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + android.defaultConfig.versionCode
- }
- }
- }
2. 從網(wǎng)絡(luò)下載當(dāng)前設(shè)備支持的SO庫
說到這里,總算回到動態(tài)加載的主題了。⊙﹏⊙
使用Android的動態(tài)加載技術(shù),可以加載外部的SO庫,所以我們可以從網(wǎng)絡(luò)下載SO庫文件并加載了。我們可以下載所有類型的SO庫文件,然后加載對應(yīng)類型的SO庫,也可以下載對應(yīng)類型的SO庫然后加載,不過無論哪種方式,我們最好都在加載SO庫前,對SO庫文件的類型做一下判斷。
我個人的方案是,存儲在服務(wù)器的SO庫依然按照APK包的壓縮方式打包,也就是,SO庫存放在APK包的 libs/xxxabi 路徑下面,下載完帶有SO庫的APK包后,我們可以遍歷libs路徑下的所有SO庫,選擇加載對應(yīng)類型的SO庫。
具體實現(xiàn)代碼看上去像是:
- /**
- * 將一個SO庫復(fù)制到指定路徑,會先檢查改SO庫是否與當(dāng)前CPU兼容
- *
- * @param sourceDir SO庫所在目錄
- * @param so SO庫名字
- * @param destDir 目標(biāo)根目錄
- * @param nativeLibName 目標(biāo)SO庫目錄名
- * @return
- */
- public static boolean copySoLib(File sourceDir, String so, String destDir, String nativeLibName) throws IOException {
- boolean isSuccess = false;
- try {
- LogUtil.d(TAG, "[copySo] 開始處理so文件");
- if (Build.VERSION.SDK_INT >= 21) {
- String[] abis = Build.SUPPORTED_ABIS;
- if (abis != null) {
- for (String abi : abis) {
- LogUtil.d(TAG, "[copySo] try supported abi:" + abi);
- String name = "lib" + File.separator + abi + File.separator + so;
- File sourceFile = new File(sourceDir, name);
- if (sourceFile.exists()) {
- LogUtil.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
- isSuccess = FileUtil.copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + nativeLibName + File.separator + so);
- //api21 64位系統(tǒng)的目錄可能有些不同
- //copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + name);
- break;
- }
- }
- } else {
- LogUtil.e(TAG, "[copySo] get abis == null");
- }
- } else {
- LogUtil.d(TAG, "[copySo] supported api:" + Build.CPU_ABI + " " + Build.CPU_ABI2);
- String name = "lib" + File.separator + Build.CPU_ABI + File.separator + so;
- File sourceFile = new File(sourceDir, name);
- if (!sourceFile.exists() && Build.CPU_ABI2 != null) {
- name = "lib" + File.separator + Build.CPU_ABI2 + File.separator + so;
- sourceFile = new File(sourceDir, name);
- if (!sourceFile.exists()) {
- name = "lib" + File.separator + "armeabi" + File.separator + so;
- sourceFile = new File(sourceDir, name);
- }
- }
- if (sourceFile.exists()) {
- LogUtil.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
- isSuccess = FileUtil.copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + nativeLibName + File.separator + so);
- }
- }
- if (!isSuccess) {
- LogUtil.e(TAG, "[copySo] 安裝 " + so + " 失敗 : NO_MATCHING_ABIS");
- throw new IOException("install " + so + " fail : NO_MATCHING_ABIS");
- }
- } catch (IOException e) {
- e.printStackTrace();
- throw e;
- }
- return true;
- }
總結(jié)
- 一種CPU架構(gòu) = 一種ABI = 一種對應(yīng)的SO庫;
- 加載SO庫時,需要加載對應(yīng)類型的SO庫;
- 盡量提供全平臺CPU類型的SO庫支持;
題外話,SO庫的使用本身就是一種最純粹的動態(tài)加載技術(shù),SO庫本身不參與APK的編譯過程,使用JNI調(diào)用SO庫里的Native方法的方式看上去也像是一種“硬編程”,Native方法看上去與一般的Java靜態(tài)方法沒什么區(qū)別,但是它的具體實現(xiàn)卻是可以隨時動態(tài)更換的(更換SO庫就好),這也可以用來實現(xiàn)熱修復(fù)的方案,與Java方法一旦加載進內(nèi)存就無法再次更換不同,Native方法不需要重啟APP就可以隨意更換。
出于安全和生態(tài)控制的原因,Google Play市場不允許APP有加載外部可執(zhí)行文件的行為,一旦你的APK里被檢查出有額外的可執(zhí)行文件時就不好玩了,所以現(xiàn)在許多APP都偷偷把用于動態(tài)加載的可執(zhí)行文件的后綴名換成“.so”,這樣被發(fā)現(xiàn)的幾率就降低了,因為加載SO庫看上去就是官方合法版本的動態(tài)加載啊(不然SO庫怎么工作),雖然這么做看起來有點掩耳盜鈴。