常用架構(gòu)
armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。
加載so的兩種方式
- 打包在apk中的情況,不需要開發(fā)者自己去判斷ABI,Android系統(tǒng)在安裝APK的時(shí)候,不會(huì)安裝APK里面全部的SO庫(kù)文件,而是會(huì)根據(jù)當(dāng)前CPU類型支持的ABI,從APK里面拷貝最合適的SO庫(kù),并保存在APP的內(nèi)部存儲(chǔ)路徑的 libs 下面。
- 動(dòng)態(tài)加載外部so的情況下,需要我們判斷ABI類型來(lái)加載相應(yīng)的so,Android系統(tǒng)不會(huì)幫我們處理。
需要注意的事項(xiàng)
一種CPU架構(gòu) = 一種對(duì)應(yīng)的ABI參數(shù) = 一種對(duì)應(yīng)類型的SO庫(kù)
比如大多的X86設(shè)備除了支持X86類型的SO庫(kù),還兼容ARM類型的SO庫(kù),所以應(yīng)用市場(chǎng)上大部分的APP只適配了ARM類型的SO庫(kù),但是注意兼容模式 運(yùn)行so庫(kù)的性能并不是很好,最好推薦還是一種abi對(duì)應(yīng)一種so庫(kù)。通過(guò) PackageManager 安裝后,在小于 Android 5.0 的系統(tǒng)中,SO庫(kù)位于 APP 的 nativeLibraryPath 目錄中;在大于等于 Android 5.0 的系統(tǒng)中,SO庫(kù)位于 APP 的 nativeLibraryRootDir/CPU_ARCH 目錄中;我們動(dòng)態(tài)加載so一般不需要關(guān)心這個(gè)問(wèn)題。
我們總是希望Android Studio 使用最新版本的build-tools來(lái)編譯,因?yàn)锳ndroid SDK最新版本會(huì)幫我們做出最優(yōu)的向下兼容工作。
但是編譯SO庫(kù) 確實(shí)正好相反的,因?yàn)镹DK平臺(tái)不是向下兼容的,而是向上兼容的。應(yīng)該使用app的minSdkVersion對(duì)應(yīng)的版本的NDK來(lái)編譯SO庫(kù)文件,如果使用了太高版本的NDK編譯,可能會(huì)導(dǎo)致APP性能低下,或者引發(fā)一些SO庫(kù)相關(guān)的運(yùn)行時(shí)異常,比如UnsatisfiedLinkError,dlopen: failed以及其他類型的Crash?,F(xiàn)在Android已經(jīng)是7.0了,目前不知道NDK是否對(duì)此有改進(jìn)。如果我們的App寫的so庫(kù)只適配了armeabi-v7a和x86架構(gòu),但是用第三方庫(kù)時(shí),第三方庫(kù)包含(armeabi-v7a,x86,ARM64),這時(shí)候某些ARM64的設(shè)備安裝該APK的時(shí)候,只要發(fā)現(xiàn)apk帶有ARM64的庫(kù),只會(huì)選擇安裝APK里面ARM64類型的SO庫(kù),這樣會(huì)導(dǎo)致我們的so庫(kù)無(wú)法拷貝到nativeLibraryPath 目錄(這種情況下不會(huì)以兼容模式找armeabi-v7a或x86下的so),所以必須保證 我們的so和第三方的so 支持的架構(gòu)類型個(gè)數(shù)匹配。 利用Android Studio很方便解決這個(gè)問(wèn)題:
一種推薦的做法
library中適配所有類型的so庫(kù)支持,app則適配少于或等于library中的so庫(kù)。利用build.gradle實(shí)現(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"
}
}
}
打包的時(shí)候以app下build.gradle支持的為準(zhǔn)。
總之一定要保持 各個(gè)庫(kù)之間對(duì)應(yīng)的架構(gòu)都是一一對(duì)應(yīng)的。
不同Android設(shè)備架構(gòu)的兼容情況
- armeabi-v7a 設(shè)備能夠加載 armeabi 指令的 so 文件
- arm64-v8a 能兼容 armeabi-v7a 和 armeabi 指令集
- x86_64 兼容 x86
- mips64 兼容 mips
mips 系 的手機(jī)設(shè)備數(shù)量太少,在項(xiàng)目里基本上不考慮。
Google提供給用戶的API
android.os.Build.SUPPORTED_ABIS
android.os.Build.CPU_ABI
android.os.Build. CPU_ABI2
這些變量用于查詢?cè)O(shè)備支持的架構(gòu),其中 SUPPORTED_ABIS 是API Level 21引入來(lái)代替CPU_ABI, CPU_ABI2的。
- 如果目標(biāo)平臺(tái)的 API Level 小于 21,只能使用 CPU_ABI 要 CPU_ABI2 來(lái)選擇了,而 CPU_ABI 要優(yōu)于 CPU_ABI2。
- API Level >=21的推薦使用android.os.Build.SUPPORTED_ABIS,但是21以下只能使用CPU_ABI 和CPU_ABI2
測(cè)試下這幾個(gè)變量的值
針對(duì)某一個(gè)固定的設(shè)備,如Nexus 9設(shè)備(arm64-v8a CPU架構(gòu))
根據(jù)前面說(shuō)描述 設(shè)備的兼容情況
SUPPORTED_ABIS 比較容易理解,返回arm64-v8a,armeabi-v7a,armeabi
而CPU_ABI 和CPU-ABI2的值不是固定不變的,它會(huì)根據(jù) APK 打包的jniLibs ,并根據(jù)設(shè)備支持的abi選擇性安裝,返回不同的值
| abiFilters(當(dāng)前apk包含的so庫(kù)類型) | 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的價(jià)值也不是很大。
activity.getApplicationInfo().nativeLibraryDir 暫時(shí)未用到
4.4-> /data/app-lib/com.less.tplayer.baidu-1
5.0-> /mnt/asec/com.less.tplayer.baidu-2/lib/arm64
動(dòng)態(tài)加載網(wǎng)絡(luò)或文件夾下的so庫(kù)
加載某文件夾 -> 相應(yīng)架構(gòu)的so文件
Apk文件本身就是一個(gè)壓縮文件,解壓后目錄結(jié)構(gòu)大致如下:

動(dòng)態(tài)加載so案例
我先把完整的代碼貼出來(lái),然后講解可能遇到的兩個(gè)錯(cuò)誤。
動(dòng)態(tài)加載的核心類(根據(jù)abi從本地選擇合適的so庫(kù)加載)
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ò)或者本地下載過(guò)的so文件夾: 選擇適合當(dāng)前設(shè)備的so文件
*
* @param soFilesDir so文件的目錄, 如apk文件解壓后的 Amusic/libs/ 目錄 : 包含[arm64-v8a,arm64-v7a等]
* @param soName so庫(kù)的文件名, 如 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 {
// 沒(méi)有找到和Build.CPU_ABI 匹配的值,那么就委屈設(shè)備使用armeabi算了.
File finnalFile = new File(soFilesDir, "armeabi" + File.separator + soName);
if (finnalFile.exists()) {
return finnalFile;
}
}
}
return null;
}
}
動(dòng)態(tài)調(diào)用so的函數(shù),不需要System.loadLibrary.
public class Security {
public native String stringFromJNI();
}
測(cè)試類,我的需要加載的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();
}
}
常見錯(cuò)誤
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
【錯(cuò)誤1】非常簡(jiǎn)單,但卻耗費(fèi)我一晚上都沒(méi)找到錯(cuò)誤,Google搜到的也是不相干的,錯(cuò)誤提示太坑了,什么是精靈魔法??我還以為是5.0版本問(wèn)題,然后測(cè)試4.0,然后以為so的寫入目錄有問(wèn)題,然后。。。
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的,啥時(shí)候bytes丟了?。?!
這個(gè)不起眼的小錯(cuò)誤差點(diǎn)搞得我放棄這個(gè)知識(shí)點(diǎn)。
上面的粗心大意的錯(cuò)誤終于解決了,卻又出現(xiàn)了下面的錯(cuò)誤,真坑!
【錯(cuò)誤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
這個(gè)問(wèn)題在Google某位大神尼古拉斯*趙四 的幫助下找到了答案:
我特意用 [vivoX9照亮你的美 arm64-v8a架構(gòu)] 測(cè)試了下:
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)先級(jí)排列的,最適合的在最上面,兼容的在下面。
前面注意事項(xiàng)中也提到過(guò):說(shuō)各個(gè)module之間so的架構(gòu)一定要對(duì)應(yīng),如果我們的App里面包含了64位的架構(gòu)arm64-v8a文件夾,那么這時(shí)候應(yīng)用的ApplicationInfo的abi就是arm64-v8a了,就會(huì)發(fā)送消息給Zygote64的進(jìn)程,創(chuàng)建的也是64位的虛擬機(jī)了,如果我們的App應(yīng)用里面只包含的是armeabi-v7a和armeabi的文件夾,那么創(chuàng)建的會(huì)是32位的虛擬機(jī)以兼容模式運(yùn)行。
我測(cè)試的時(shí)候,app里面并沒(méi)有任何so文件,但是動(dòng)態(tài)加載本地armeabi-v7a架構(gòu)so的時(shí)候卻出現(xiàn)這種錯(cuò)誤,后來(lái)推斷:
如果App里面沒(méi)有任何so文件,那么默認(rèn)就以該手機(jī)最適合的模式即arm64-v8a運(yùn)行。但是注意了,64位的虛擬機(jī)不能運(yùn)行32位的so。
【錯(cuò)誤3】
64位的so文件也不能運(yùn)行在32位的虛擬機(jī)中。
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
雖然動(dòng)態(tài)加載so的時(shí)候,我本地放入arm64-v8a就不會(huì)報(bào)錯(cuò)

但是,如果后期想動(dòng)態(tài)加載第三方的庫(kù)(如極光推送),這些庫(kù)里面沒(méi)有arm64-v8a,或者有的手機(jī)不支持arm64-v8a,那么一加載程序就崩了。
根據(jù)上面的推論:
我想到的一種方式是:本地保留的 >= 宿主App(但至少有一個(gè))用于欺騙Android設(shè)備。


這樣系統(tǒng)發(fā)現(xiàn)該App含有armeabi-v7a的文件夾(里面需要有空so文件),那么就會(huì)以兼容模式啟動(dòng)32位虛擬機(jī),然后根據(jù)本地目錄文件夾,結(jié)合上面給出代碼 選擇so的順序邏輯加載 需要的so文件。
注意:此時(shí)是按32位虛擬機(jī)啟動(dòng)的,本地so的文件夾里面你可千萬(wàn)別沒(méi)事找事添加arm64-v8a文件夾了,否則就會(huì)發(fā)生【錯(cuò)誤3】了。
總結(jié)
- 32位的so文件不能運(yùn)行在64位的虛擬機(jī)中。
- 同理:64位的so文件也不能運(yùn)行在32位的虛擬機(jī)中。
參考:
Android動(dòng)態(tài)加載補(bǔ)充 加載SD卡中的SO庫(kù)
ANDROID動(dòng)態(tài)加載 使用SO庫(kù)時(shí)要注意的一些問(wèn)題
動(dòng)態(tài)鏈接庫(kù)加載原理及HotFix方案介紹
android loadlibrary 更改libPath 路徑,指定路徑加載.so
Android中so使用知識(shí)和問(wèn)題總結(jié)