常用架構(gòu)
armeabi薄腻,armeabi-v7a,x86届案,mips庵楷,arm64-v8a,mips64,x86_64尽纽。
加載so的兩種方式
- 打包在apk中的情況咐蚯,不需要開發(fā)者自己去判斷ABI,Android系統(tǒng)在安裝APK的時候弄贿,不會安裝APK里面全部的SO庫文件春锋,而是會根據(jù)當前CPU類型支持的ABI,從APK里面拷貝最合適的SO庫差凹,并保存在APP的內(nèi)部存儲路徑的 libs 下面期奔。
- 動態(tài)加載外部so的情況下,需要我們判斷ABI類型來加載相應(yīng)的so危尿,Android系統(tǒng)不會幫我們處理呐萌。
需要注意的事項
一種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庫济欢。通過 PackageManager 安裝后赠堵,在小于 Android 5.0 的系統(tǒng)中,SO庫位于 APP 的 nativeLibraryPath 目錄中船逮;在大于等于 Android 5.0 的系統(tǒng)中顾腊,SO庫位于 APP 的 nativeLibraryRootDir/CPU_ARCH 目錄中;我們動態(tài)加載so一般不需要關(guān)心這個問題挖胃。
我們總是希望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是否對此有改進狼牺。如果我們的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)的兼容情況
- armeabi-v7a 設(shè)備能夠加載 armeabi 指令的 so 文件
- arm64-v8a 能兼容 armeabi-v7a 和 armeabi 指令集
- x86_64 兼容 x86
- 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
的音诈。
- 如果目標平臺的 API Level 小于 21,只能使用 CPU_ABI 要 CPU_ABI2 來選擇了绎狭,而 CPU_ABI 要優(yōu)于 CPU_ABI2细溅。
- 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)大致如下:
動態(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就不會報錯
但是废士,如果后期想動態(tài)加載第三方的庫(如極光推送),這些庫里面沒有arm64-v8a蝇完,或者有的手機不支持arm64-v8a官硝,那么一加載程序就崩了矗蕊。
根據(jù)上面的推論:
我想到的一種方式是:本地保留的 >= 宿主App(但至少有一個)用于欺騙Android設(shè)備。
這樣系統(tǒng)發(fā)現(xiàn)該App含有armeabi-v7a的文件夾(里面需要有空so文件)氢架,那么就會以兼容模式啟動32位虛擬機傻咖,然后根據(jù)本地目錄文件夾,結(jié)合上面給出代碼 選擇so的順序邏輯加載 需要的so文件岖研。
注意:此時是按32位虛擬機啟動的卿操,本地so的文件夾里面你可千萬別沒事找事添加arm64-v8a文件夾了,否則就會發(fā)生【錯誤3】了孙援。
總結(jié)
- 32位的so文件不能運行在64位的虛擬機中害淤。
- 同理:64位的so文件也不能運行在32位的虛擬機中。
參考:
Android動態(tài)加載補充 加載SD卡中的SO庫
ANDROID動態(tài)加載 使用SO庫時要注意的一些問題
動態(tài)鏈接庫加載原理及HotFix方案介紹
android loadlibrary 更改libPath 路徑拓售,指定路徑加載.so
Android中so使用知識和問題總結(jié)