Android使用 SO 库时要注意的一些问题
常和 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 庫會異常卡頓(試著回想幾年前你開始學(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 庫。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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(引用自別的文章,沒實際使用過):
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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)代碼看上去像是:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | /** * 將一個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é)
題外話,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 庫怎么工作),雖然這么做看起來有點掩耳盜鈴。
轉(zhuǎn)載于:https://www.cnblogs.com/android-blogs/p/5867337.html
總結(jié)
以上是生活随笔為你收集整理的Android使用 SO 库时要注意的一些问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 110. Balanced Binary
- 下一篇: Android开发7:简单的数据存储(使