做者:kaedeajava
項目:android-dynamical-loadingandroid
正好動態加載系列文章談到了加載SO庫的地方,我以爲這裏能夠順便談談使用SO庫時須要注意的一些問題。或許這些問題對於常常和SO庫開發打交道的同窗來講已是老生長談,可是既然要討論一整個動態加載系列,我想仍是有必要說說使用SO庫時的一些問題。git
在項目裏使用SO庫很是簡單,在 加載SD卡中的SO庫 中也有談到,只須要把須要用到的SO庫拷貝進 jniLibs(或者Eclipse項目裏面的libs) 中,而後在JAVA代碼中調用 System.loadLibrary("xxx") 加載對應的SO庫,就可使用JNI語句調用SO庫裏面的Native方法了。github
可是有同窗注意到了,SO庫文件能夠隨便改文件名,卻不能任意修改文件夾路徑,而是「armeabi」、「armeabi-v7a」、「x86」等文件夾名有着嚴格的要求,這些文件夾名有什麼意義麼?web
緣由很簡單,不一樣CPU架構的設備須要用不一樣類型SO庫(從文件名也能夠猜出來個大概嘛 ╮( ̄▽ ̄")╭)。編程
記得還在學校的時候,說起ARM處理器時,老師說之後移動設備的CPU基本就是ARM類型的了。老師未曾欺我,早期的Android系統幾乎只支持ARM的CPU架構,不過如今至少支持如下七種不一樣的CPU架構:ARMv5,ARMv7,x86,MIPS,ARMv8,MIPS64和x86_64。每一種CPU類型都對應一種ABI(Application Binary Interface),「armeabi-v7a」文件夾前面的「armeabi」指的就是ARM這種類型的ABI,後面的「v7a」指的是ARMv7。這7種CPU類型對應的SO庫的文件夾名是:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。api
不一樣類型的移動設備在運行APP時,須要加載本身支持的類型的SO庫,否則就GG了。經過 Build.SUPPORTED_ABIS 咱們能夠判斷當前設備支持的ABI,不過通常狀況下,不須要開發者本身去判斷ABI,Android系統在安裝APK的時候,不會安裝APK裏面所有的SO庫文件,而是會根據當前CPU類型支持的ABI,從APK裏面拷貝最合適的SO庫,並保存在APP的內部存儲路徑的 libs 下面。(這裏說通常狀況,是由於有例外的狀況存在,好比咱們動態加載外部的SO庫的時候,就須要本身判斷ABI類型了。)安全
一種CPU架構 = 一種對應的ABI參數 = 一種對應類型的SO庫服務器
到這裏,咱們發現使用SO庫的邏輯仍是比較簡單的,可是Android系統加載SO庫的邏輯仍是給咱們留下了一些坑。網絡
SO庫其實都是APP運行時加載的,也就是說APP只有在運行的時候才知道SO庫文件的存在,這就沒法經過靜態代碼檢查或者在編譯APP時檢查SO庫文件是否正常。因此,Android開發對SO庫的存放路徑有嚴格的要求。
使用SO庫的時候,除了「armeabi-v7a」等文件夾名須要嚴格按照規定的來自外,SO庫要放在項目的哪一個文件夾下也要按照套路來,如下是一些總結:
Android Studio 工程放在 jniLibs/xxxabi 目錄中(固然也能夠經過在build.gradle文件中的設置jniLibs.srcDir屬性本身指定);
Eclipse 工程放在 libs/xxxabi 目錄中(這也是使用ndk-build命令生成SO庫的默認目錄);
aar 依賴包中位於 jni/ABI 目錄中(SO庫會自動包含到引用AAR壓縮包到APK中);
最終構建出來的APK文件中,SO庫存在 lib/xxxabi 目錄中(也就是說不管你用什麼方式構建,只要保證APK包裏SO庫的這個路徑沒錯就沒問題);
經過 PackageManager 安裝後,在小於 Android 5.0 的系統中,SO庫位於 APP 的 nativeLibraryPath 目錄中;在大於等於 Android 5.0 的系統中,SO庫位於 APP 的 nativeLibraryRootDir/CPU_ARCH 目錄中;
既然扯到了這裏,順便說一下,我在使用 Android Studio 1.5 構建APK的時候,發現 Gradle 插件只會默認打包application類型的module的jniLibs下面的SO庫文件,而不會打包aar依賴包的SO庫,因此會致使最終構建出來的APK裏的SO庫文件缺失。暫時的解決方案是把全部的SO庫都放在application模塊中(這顯然不是很好的解決方案),不知道這是否是Studio的BUG,同事的解決方案是經過修改Gradle插件來增長對aar依賴包的SO庫的打包支持(GitHub有開源的第三方Gradle插件項目,使用Java和Groovy語言開發)。
當一個應用安裝在設備上,只有該設備支持的CPU架構對應的SO庫會被安裝。可是,有時候,設備支持的SO庫類型不止一種,好比大多的X86設備除了支持X86類型的SO庫,還兼容ARM類型的SO庫(目前應用市場上大部分的APP只適配了ARM類型的SO庫,X86類型的設備若是不能兼容ARM類型的SO庫的話,大概要嗝屁了吧)。
因此若是你的APK只適配了ARM類型的SO庫的話,仍是能以兼容的模式在X86類型的設備上運行(好比華碩的平板),可是這不意味着你就不用適配X86類型的SO庫了,由於X86的CPU使用兼容模式運行ARM類型的SO庫會異常卡頓(試着回想幾年前你開始學習Android開發的時候,在PC上使用AVD模擬器的那種感受)。
除了要注意使用了正確CPU類型的SO庫,也要注意SO庫的編譯版本的問題。雖然如今的Android Studio支持在項目中直接編譯SO庫,可是更多的時候咱們仍是選擇使用事先編譯好的SO庫,這時就要注意了,編譯APK的時候,咱們老是但願使用最新版本的build-tools來編譯,由於Android SDK最新版本會幫咱們作出最優的向下兼容工做。
可是這對於編譯SO庫來講就不同了,由於NDK平臺不是向下兼容的,而是向上兼容的。應該使用app的minSdkVersion對應的版本的NDK標原本編譯SO庫文件,若是使用了過高版本的NDK,可能會致使APP性能低下,或者引起一些SO庫相關的運行時異常,好比「UnsatisfiedLinkError」,「dlopen: failed」以及其餘類型的Crash。
通常狀況下,咱們都是使用編譯好的SO庫文件,因此當你引入一個預編譯好的SO庫時,你須要檢查它被編譯所用的平臺版本。
好比有時候,由於業務的需求,咱們的APP不須要支持AMR64的設備,但這不意味着咱們就不用編譯ARM64對應的SO庫。舉個例子,咱們的APP只支持armeabi-v7a和x86架構,而後咱們的APP使用了一個第三方的Library,而這個Library提供了AMR64等更多類型CPU架構的支持,構建APK的時候,這些ARM64的SO庫依然會被打包進APK裏面,也就是說咱們本身的SO庫沒有對應的ARM64的SO庫,而第三方的Library卻有。這時候,某些ARM64的設備安裝該APK的時候,發現咱們的APK裏帶有ARM64的SO庫,會誤覺得咱們的APP已經作好了AMR64的適配工做,因此只會選擇安裝APK裏面ARM64類型的SO庫,這樣會致使咱們本身項目的SO庫沒有被正確安裝(雖然armeabi-v7a和x86類型的SO庫確實存在APK包裏面)。
這時正確的作法是,給咱們本身的SO庫也提供AMR64支持,或者不打包第三方Library項目的ARM64的SO庫。使用第二種方案時,能夠把APK裏面不須要支持的ABI文件夾給刪除,而後從新打包,而在Android Studio下,則能夠經過如下的構建方式指定須要類型的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能支持的設備CPU類型的數量,就是項目中全部SO庫支持的最少CPU類型的數量(使用咱們SDK的APP能支持的CPU類型只能少於等於咱們SDK支持的類型)。
確實,全部的x86/x86_64/armeabi-v7a/arm64-v8a設備都支持armeabi架構的SO庫,所以彷佛移除其餘ABIs的SO庫是一個減小APK大小的好辦法。但事實上並非,這不僅影響到函數庫的性能和兼容性。
X86設備可以很好的運行ARM類型函數庫,但並不保證100%不發生crash,特別是對舊設備,兼容只是一種保底方案。64位設備(arm64-v8a, x86_64, mips64)可以運行32位的函數庫,可是以32位模式運行,在64位平臺上運行32位版本的ART和Android組件,將丟失專爲64位優化過的性能(ART,webview,media等等)。
過減小其餘CPU類型支持的SO庫來減小APK的體積不是很明智的作法,若是真的須要經過減小SO庫來作APK瘦身,咱們也有其餘辦法。
咱們能夠構建一個APK,它支持全部的CPU類型。可是反過來,咱們能夠爲每一個CPU類型都單獨構建一個APK,而後不一樣CPU類型的設備安裝對應的APK便可,固然前提是應用市場得提供用戶設備CPU類型設別的支持,就目前來講,至少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 } } }
說到這裏,總算回到動態加載的主題了。⊙﹏⊙
使用Android的動態加載技術,能夠加載外部的SO庫,因此咱們能夠從網絡下載SO庫文件並加載了。咱們能夠下載全部類型的SO庫文件,而後加載對應類型的SO庫,也能夠下載對應類型的SO庫而後加載,不過不管哪一種方式,咱們最好都在加載SO庫前,對SO庫文件的類型作一下判斷。
我我的的方案是,存儲在服務器的SO庫依然按照APK包的壓縮方式打包,也就是,SO庫存放在APK包的 libs/xxxabi 路徑下面,下載完帶有SO庫的APK包後,咱們能夠遍歷libs路徑下的全部SO庫,選擇加載對應類型的SO庫。
具體實現代碼看上去像是:
/** * 將一個SO庫複製到指定路徑,會先檢查改SO庫是否與當前CPU兼容 * * @param sourceDir SO庫所在目錄 * @param so SO庫名字 * @param destDir 目標根目錄 * @param nativeLibName 目標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位系統的目錄可能有些不一樣 //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; }
一種CPU架構 = 一種ABI = 一種對應的SO庫;
加載SO庫時,須要加載對應類型的SO庫;
儘可能提供全平臺CPU類型的SO庫支持;
題外話,SO庫的使用自己就是一種最純粹的動態加載技術,SO庫自己不參與APK的編譯過程,使用JNI調用SO庫裏的Native方法的方式看上去也像是一種「硬編程」,Native方法看上去與通常的Java靜態方法沒什麼區別,可是它的具體實現倒是能夠隨時動態更換的(更換SO庫就好),這也能夠用來實現熱修復的方案,與Java方法一旦加載進內存就沒法再次更換不一樣,Native方法不須要重啓APP就能夠隨意更換。
出於安全和生態控制的緣由,Google Play市場不容許APP有加載外部可執行文件的行爲,一旦你的APK裏被檢查出有額外的可執行文件時就很差玩了,因此如今許多APP都偷偷把用於動態加載的可執行文件的後綴名換成「.so」,這樣被發現的概率就下降了,由於加載SO庫看上去就是官方合法版本的動態加載啊(否則SO庫怎麼工做),雖然這麼作看起來有點掩耳盜鈴。