動態(tài)加載so注意事項&案例

常用架構(gòu)
armeabi薄腻,armeabi-v7a,x86届案,mips庵楷,arm64-v8a,mips64,x86_64尽纽。

加載so的兩種方式

  1. 打包在apk中的情況咐蚯,不需要開發(fā)者自己去判斷ABI,Android系統(tǒng)在安裝APK的時候弄贿,不會安裝APK里面全部的SO庫文件春锋,而是會根據(jù)當前CPU類型支持的ABI,從APK里面拷貝最合適的SO庫差凹,并保存在APP的內(nèi)部存儲路徑的 libs 下面期奔。
  2. 動態(tài)加載外部so的情況下,需要我們判斷ABI類型來加載相應(yīng)的so危尿,Android系統(tǒng)不會幫我們處理呐萌。

需要注意的事項

  1. 一種CPU架構(gòu) = 一種對應(yīng)的ABI參數(shù) =  一種對應(yīng)類型的SO庫
    比如大多的X86設(shè)備除了支持X86類型的SO庫,還兼容ARM類型的SO庫谊娇,所以應(yīng)用市場上大部分的APP只適配了ARM類型的SO庫肺孤,但是注意兼容模式 運行so庫的性能并不是很好,最好推薦還是一種abi對應(yīng)一種so庫济欢。

  2. 通過 PackageManager 安裝后赠堵,在小于 Android 5.0 的系統(tǒng)中,SO庫位于 APP 的 nativeLibraryPath 目錄中船逮;在大于等于 Android 5.0 的系統(tǒng)中顾腊,SO庫位于 APP 的 nativeLibraryRootDir/CPU_ARCH 目錄中;我們動態(tài)加載so一般不需要關(guān)心這個問題挖胃。

  3. 我們總是希望Android Studio 使用最新版本的build-tools來編譯杂靶,因為Android SDK最新版本會幫我們做出最優(yōu)的向下兼容工作。
    但是編譯SO庫 確實正好相反的酱鸭,因為NDK平臺不是向下兼容的吗垮,而是向上兼容的。應(yīng)該使用app的minSdkVersion對應(yīng)的版本的NDK來編譯SO庫文件凹髓,如果使用了太高版本的NDK編譯烁登,可能會導(dǎo)致APP性能低下,或者引發(fā)一些SO庫相關(guān)的運行時異常蔚舀,比如UnsatisfiedLinkError饵沧,dlopen: failed以及其他類型的Crash。現(xiàn)在Android已經(jīng)是7.0了赌躺,目前不知道NDK是否對此有改進狼牺。

  4. 如果我們的App寫的so庫只適配了armeabi-v7a和x86架構(gòu),但是用第三方庫時礼患,第三方庫包含(armeabi-v7a是钥,x86掠归,ARM64),這時候某些ARM64的設(shè)備安裝該APK的時候悄泥,只要發(fā)現(xiàn)apk帶有ARM64的庫虏冻,只會選擇安裝APK里面ARM64類型的SO庫,這樣會導(dǎo)致我們的so庫無法拷貝到nativeLibraryPath 目錄(這種情況下不會以兼容模式找armeabi-v7a或x86下的so)弹囚,所以必須保證 我們的so和第三方的so 支持的架構(gòu)類型個數(shù)匹配厨相。 利用Android Studio很方便解決這個問題:

一種推薦的做法
library中適配所有類型的so庫支持,app則適配少于或等于library中的so庫余寥。利用build.gradle實現(xiàn)领铐。

app下的build.gradle

productFlavors {
    flavor {
        ndk {
            abiFilters "armeabi-v7a"
            abiFilters "x86"
            abiFilters "armeabi"
        }
    }
}

library下的build.gradle

productFlavors {
    flavor {
        ndk {
            abiFilters "armeabi-v7a"
            abiFilters "x86"
            abiFilters "armeabi"
            abiFilters "arm64-v8a"
            abiFilters "x86_64"
        }
    }
}

打包的時候以app下build.gradle支持的為準。
總之一定要保持 各個庫之間對應(yīng)的架構(gòu)都是一一對應(yīng)的宋舷。

不同Android設(shè)備架構(gòu)的兼容情況

  1. armeabi-v7a 設(shè)備能夠加載 armeabi 指令的 so 文件
  2. arm64-v8a 能兼容 armeabi-v7a 和 armeabi 指令集
  3. x86_64 兼容 x86
  4. mips64 兼容 mips
    mips 系 的手機設(shè)備數(shù)量太少绪撵,在項目里基本上不考慮。

Google提供給用戶的API

android.os.Build.SUPPORTED_ABIS
android.os.Build.CPU_ABI 
android.os.Build. CPU_ABI2 

這些變量用于查詢設(shè)備支持的架構(gòu)祝蝠,其中 SUPPORTED_ABIS 是API Level 21引入來代替CPU_ABI, CPU_ABI2的音诈。

  1. 如果目標平臺的 API Level 小于 21,只能使用 CPU_ABI 要 CPU_ABI2 來選擇了绎狭,而 CPU_ABI 要優(yōu)于 CPU_ABI2细溅。
  2. API Level >=21的推薦使用android.os.Build.SUPPORTED_ABIS,但是21以下只能使用CPU_ABI 和CPU_ABI2

測試下這幾個變量的值
針對某一個固定的設(shè)備儡嘶,如Nexus 9設(shè)備(arm64-v8a CPU架構(gòu))

根據(jù)前面說描述 設(shè)備的兼容情況
SUPPORTED_ABIS 比較容易理解喇聊,返回arm64-v8a,armeabi-v7a,armeabi

而CPU_ABI 和CPU-ABI2的值不是固定不變的,它會根據(jù) APK 打包的jniLibs 蹦狂,并根據(jù)設(shè)備支持的abi選擇性安裝誓篱,返回不同的值

abiFilters(當前apk包含的so庫類型) CPU_ABI CPU_ABI2
armeabi-v7a, arm64-v8a, armeabi arm64-v8a
arm64-v8a, armeabi-v7a arm64-v8a
arm64-v8a,armeabi arm64-v8a
arm64-v8a
armeabi armeabi-v7a armeabi
armeabi-v7a armeabi-v7a armeabi

所以5.0(21)以上,推薦使用android.os.Build.SUPPORTED_ABIS判斷獲取設(shè)備支持的架構(gòu)類型凯楔,而5.0以下則使用android.os.Build.CPU_ABI 即可窜骄,android.os.Build.CPU_ABI2的價值也不是很大。

activity.getApplicationInfo().nativeLibraryDir 暫時未用到

4.4-> /data/app-lib/com.less.tplayer.baidu-1
5.0-> /mnt/asec/com.less.tplayer.baidu-2/lib/arm64

動態(tài)加載網(wǎng)絡(luò)或文件夾下的so庫

加載某文件夾 -> 相應(yīng)架構(gòu)的so文件

Apk文件本身就是一個壓縮文件摆屯,解壓后目錄結(jié)構(gòu)大致如下:

某apk文件的解壓目錄

動態(tài)加載so案例

我先把完整的代碼貼出來邻遏,然后講解可能遇到的兩個錯誤。

動態(tài)加載的核心類(根據(jù)abi從本地選擇合適的so庫加載)

public class DynamicSO {
    private static final String TAG = DynamicSO.class.getSimpleName();

    public static void loadExSo(Context context,String soName, String soFilesDir){
        File soFile = choose(soFilesDir,soName);

        String destFileName = context.getDir("myso", Context.MODE_PRIVATE).getAbsolutePath()  + File.separator + soName;
        File destFile = new File(destFileName);
        if (soFile != null) {
            Log.e(TAG, "最終選擇加載的so路徑: " + soFile.getAbsolutePath());
            Log.e(TAG, "寫入so的路徑: " + destFileName);
            boolean flag = FileUtil.copyFile(soFile, destFile);
            if (flag) {
                System.load(destFileName);
            }
        }

    }

    /**
     * 在網(wǎng)絡(luò)或者本地下載過的so文件夾: 選擇適合當前設(shè)備的so文件
     *
     * @param soFilesDir so文件的目錄, 如apk文件解壓后的 Amusic/libs/ 目錄 : 包含[arm64-v8a,arm64-v7a等]
     * @param soName so庫的文件名, 如 libmusic.so
     * @return 最終匹配合適的so文件
     */
    private static File choose(String soFilesDir,String soName) {
        if (Build.VERSION.SDK_INT >= 21) {
            String [] abis = Build.SUPPORTED_ABIS;
            for (String abi : abis) {
                Log.e(TAG, "SUPPORTED_ABIS =============> " + abi);
            }
            for (String abi : abis) {
                File file = new File(soFilesDir,abi + File.separator + soName);
                if (file.exists()) {
                    return file;
                }
            }
        } else {
            File file = new File(soFilesDir, Build.CPU_ABI + File.separator + soName);
            if (file.exists()) {
                return file;
            } else {
                // 沒有找到和Build.CPU_ABI 匹配的值,那么就委屈設(shè)備使用armeabi算了.
                File finnalFile = new File(soFilesDir, "armeabi" + File.separator + soName);
                if (finnalFile.exists()) {
                    return finnalFile;
                }
            }
        }
        return null;
    }
}

動態(tài)調(diào)用so的函數(shù)虐骑,不需要System.loadLibrary.

public class Security {

    public native String stringFromJNI();
}

測試類准验,我的需要加載的so文件都是放在sdcard/mylibs目錄下的。

public class TestActivity extends AppCompatActivity {

    public void handle(View view) {
        DynamicSO.loadExSo(this,"libnative-lib.so", Environment.getExternalStorageDirectory() + "/mylibs");
        // JNI 調(diào)用
        Security security = new Security();
        String message = security.stringFromJNI();
        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
    }
}

常見錯誤

1. Exception: dlopen failed: "/data/data/com.less.tplayer.baidu/app_myso/libnative-lib.so" has bad ELF magic
2. E/art: dlopen("/data/data/com.less.tplayer.baidu/app_myso/libnative-lib.so", RTLD_LAZY) failed: dlopen failed: "/data/data/com.less.tplayer.baidu/app_myso/libnative-lib.so" is 32-bit instead of 64-bit
3. E/art: dlopen("/data/data/com.less.tplayer.baidu/app_myso/libnative-lib.so", RTLD_LAZY) failed: dlopen failed: "/data/data/com.less.tplayer.baidu/app_myso/libnative-lib.so" is 64-bit instead of 32-bit

【錯誤1】非常簡單廷没,但卻耗費我一晚上都沒找到錯誤沟娱,Google搜到的也是不相干的,錯誤提示太坑了腕柜,什么是精靈魔法??我還以為是5.0版本問題盏缤,然后測試4.0砰蠢,然后以為so的寫入目錄有問題,然后唉铜。台舱。。

byte[] bytes = new byte[1024];
int len = -1;
while ( (len = bufferedInputStream.read(bytes)) != -1) {
    bufferedOutputStream.write(bytes, 0, len);
}

拐了一大圈潭流,最后是

bufferedInputStream.read(bytes)
bufferedInputStream.read()

TNN的竞惋,啥時候bytes丟了!;壹怠拆宛!
這個不起眼的小錯誤差點搞得我放棄這個知識點。
上面的粗心大意的錯誤終于解決了讼撒,卻又出現(xiàn)了下面的錯誤浑厚,真坑!

【錯誤2】

E/art: dlopen("/data/data/com.less.tplayer.baidu/app_myso/libnative-lib.so", RTLD_LAZY) failed: dlopen failed: "/data/data/com.less.tplayer.baidu/app_myso/libnative-lib.so" is 32-bit instead of 64-bit

這個問題在Google某位大神尼古拉斯*趙四 的幫助下找到了答案:

我特意用 [vivoX9照亮你的美 arm64-v8a架構(gòu)] 測試了下:

String [] abis = Build.SUPPORTED_ABIS;
    for (String abi : abis) {
      Log.e(TAG, "SUPPORTED_ABIS =============> " + abi);
  }
}

打印結(jié)果是:

E/DynamicSO: SUPPORTED_ABIS =============> arm64-v8a
E/DynamicSO: SUPPORTED_ABIS =============> armeabi-v7a
E/DynamicSO: SUPPORTED_ABIS =============> armeabi

這些順序是按照優(yōu)先級排列的根盒,最適合的在最上面钳幅,兼容的在下面。

前面注意事項中也提到過:說各個module之間so的架構(gòu)一定要對應(yīng)炎滞,如果我們的App里面包含了64位的架構(gòu)arm64-v8a文件夾敢艰,那么這時候應(yīng)用的ApplicationInfo的abi就是arm64-v8a了,就會發(fā)送消息給Zygote64的進程册赛,創(chuàng)建的也是64位的虛擬機了钠导,如果我們的App應(yīng)用里面只包含的是armeabi-v7a和armeabi的文件夾,那么創(chuàng)建的會是32位的虛擬機以兼容模式運行击奶。

我測試的時候辈双,app里面并沒有任何so文件,但是動態(tài)加載本地armeabi-v7a架構(gòu)so的時候卻出現(xiàn)這種錯誤柜砾,后來推斷:

如果App里面沒有任何so文件湃望,那么默認就以該手機最適合的模式即arm64-v8a運行。但是注意了痰驱,64位的虛擬機不能運行32位的so证芭。

【錯誤3】
64位的so文件也不能運行在32位的虛擬機中。

E/art: dlopen("/data/data/com.less.tplayer.baidu/app_myso/libnative-lib.so", RTLD_LAZY) failed: dlopen failed: "/data/data/com.less.tplayer.baidu/app_myso/libnative-lib.so" is 64-bit instead of 32-bit

雖然動態(tài)加載so的時候担映,我本地放入arm64-v8a就不會報錯

加載64位的so

但是废士,如果后期想動態(tài)加載第三方的庫(如極光推送),這些庫里面沒有arm64-v8a蝇完,或者有的手機不支持arm64-v8a官硝,那么一加載程序就崩了矗蕊。

根據(jù)上面的推論:
我想到的一種方式是:本地保留的 >= 宿主App(但至少有一個)用于欺騙Android設(shè)備。

本地保留的 >= 宿主App(但至少有一個)
宿主建立一個空的32位版的空so

這樣系統(tǒng)發(fā)現(xiàn)該App含有armeabi-v7a的文件夾(里面需要有空so文件)氢架,那么就會以兼容模式啟動32位虛擬機傻咖,然后根據(jù)本地目錄文件夾,結(jié)合上面給出代碼 選擇so的順序邏輯加載 需要的so文件岖研。

注意:此時是按32位虛擬機啟動的卿操,本地so的文件夾里面你可千萬別沒事找事添加arm64-v8a文件夾了,否則就會發(fā)生【錯誤3】了孙援。


總結(jié)

  1. 32位的so文件不能運行在64位的虛擬機中害淤。
  2. 同理:64位的so文件也不能運行在32位的虛擬機中。

參考:
Android動態(tài)加載補充 加載SD卡中的SO庫
ANDROID動態(tài)加載 使用SO庫時要注意的一些問題
動態(tài)鏈接庫加載原理及HotFix方案介紹
android loadlibrary 更改libPath 路徑拓售,指定路徑加載.so
Android中so使用知識和問題總結(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末窥摄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子邻辉,更是在濱河造成了極大的恐慌溪王,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件值骇,死亡現(xiàn)場離奇詭異莹菱,居然都是意外死亡,警方通過查閱死者的電腦和手機吱瘩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進店門道伟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人使碾,你說我怎么就攤上這事蜜徽。” “怎么了票摇?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵拘鞋,是天一觀的道長。 經(jīng)常有香客問我矢门,道長盆色,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任祟剔,我火速辦了婚禮隔躲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘物延。我一直安慰自己宣旱,他們只是感情好,可當我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布叛薯。 她就那樣靜靜地躺著浑吟,像睡著了一般笙纤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上组力,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天粪糙,我揣著相機與錄音,去河邊找鬼忿项。 笑死,一個胖子當著我的面吹牛城舞,可吹牛的內(nèi)容都是我干的轩触。 我是一名探鬼主播,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼家夺,長吁一口氣:“原來是場噩夢啊……” “哼脱柱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拉馋,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤榨为,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后煌茴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體随闺,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年蔓腐,在試婚紗的時候發(fā)現(xiàn)自己被綠了矩乐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡回论,死狀恐怖散罕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情傀蓉,我是刑警寧澤欧漱,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站葬燎,受9級特大地震影響误甚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜萨蚕,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一靶草、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧岳遥,春花似錦奕翔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宾袜。三九已至,卻和暖如春驾窟,著一層夾襖步出監(jiān)牢的瞬間庆猫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工绅络, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留月培,地道東北人。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓恩急,卻偏偏與公主長得像杉畜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子衷恭,可洞房花燭夜當晚...
    茶點故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容