一般情況下,我們不需要關(guān)心so缠捌。但是當(dāng)APP使用的第三方SDK中包含了so文件锄贷,或者自己需要使用NDK開發(fā)某些功能,就有必要去好好了解下so的一些知識曼月。
出處: Allen's Zone
作者: Allen Feng
什么是ABI和so
早期的Android設(shè)備只支持ARMv5的CPU架構(gòu)谊却,隨著Android系統(tǒng)的快速發(fā)展,搭載Android的硬件平臺也早已多樣化了哑芹,又加入了ARMv7炎辨,x86,MIPS聪姿,ARMv8碴萧,MIPS64和x86_64乙嘀。
每一種CPU架構(gòu),都定義了一種ABI(Application Binary Interface破喻,應(yīng)用二進(jìn)制接口)乒躺,ABI定義了其所對應(yīng)的CPU架構(gòu)能夠執(zhí)行的二進(jìn)制文件(如.so文件)的格式規(guī)范,決定了二進(jìn)制文件如何與系統(tǒng)進(jìn)行交互低缩。
每一種ABI的詳細(xì)介紹可以參見官方的介紹ABI Management。
so(shared object曹货,共享庫)是機(jī)器可以直接運(yùn)行的二進(jìn)制代碼咆繁,是Android上的動(dòng)態(tài)鏈接庫,類似于Windows上的dll顶籽。每一個(gè)Android應(yīng)用所支持的ABI是由其APK提供的.so文件決定的玩般,這些so文件被打包在apk文件的lib/<abi>目錄下,其中abi可以是上面表格中的一個(gè)或者多個(gè)礼饱。
例如坏为,解壓一個(gè)apk文件后,在lib目錄下可以看到如下文件:
lib
|
├── armeabi
│ └── libmath.so
├── armeabi-v7a
│ └── libmath.so
├── mips
│ └── libmath.so
└── x86
└── libmath.so
說明該應(yīng)用所支持的ABI為armeabi, armeabi-v7a, mips, 和x86镊绪。
注:可以使用aapt
命令快速查看apk支持的abi
~ aapt dump badging baidutieba.apk | grep abi
native-code: 'armeabi' 'mips' 'x86'
為什么使用so
- so機(jī)制讓開發(fā)者最大化利用已有的C和C++代碼匀伏,達(dá)到重用的效果,利用軟件世界積累了幾十年的優(yōu)秀代碼蝴韭;
- so是二進(jìn)制够颠,沒有解釋編譯的開消,用so實(shí)現(xiàn)的功能比純java實(shí)現(xiàn)的功能要快榄鉴;
- so內(nèi)存分配不受Dalivik/ART的單個(gè)應(yīng)用限制履磨,減少OOM;
- 相對于java代碼庆尘,二進(jìn)制代碼的反編譯難度更大剃诅,一些核心代碼可以考慮放在so中。
為指定的ABI生成so
默認(rèn)情況下驶忌,NDK只會為armeabi生成.so文件矛辕,若需要生成支持其他ABI的.so文件,可以在Application.mk文件中指定APP_ABI
參數(shù):
APP_ABI := armeabi-v7a
APP_ABI
參數(shù)可以被指定多個(gè)值以支持多個(gè)ABI:
APP_ABI := armeabi armeabi-v7a x86
當(dāng)然位岔,你也可以使用all
來生成支持所有ABI的so:
APP_ABI := all
查看Android系統(tǒng)的ABI支持
Android可以在運(yùn)行期間確定當(dāng)前系統(tǒng)所支持的ABI如筛,這是由系統(tǒng)編譯時(shí)的具體參數(shù)指定的:
-
primary ABI
(主ABI):對應(yīng)當(dāng)前系統(tǒng)中使用的機(jī)器碼類型 -
secondary ABI
(副ABI):表示當(dāng)前系統(tǒng)支持的其他ABI類型
許多手機(jī)支持不止一個(gè)ABI,比如抒抬,一個(gè)基于ARMv7的設(shè)備會將armeabi-v7a定義為primary ABI杨刨,armeabi作為secondary ABI,意味著這臺機(jī)器同時(shí)支持armeabi-v7a和armeabi擦剑。
許多基于x86的設(shè)備也可以運(yùn)行armeabi-v7a和armeabi的so妖胀,對于這些機(jī)器芥颈,primary ABI是x86,secondary ABI則是armeabi-v7a.
但是赚抡,為了能得到更好的性能表現(xiàn)爬坑,我們應(yīng)該盡可能的直接提供primary ABI所對應(yīng)的so文件。比如涂臣,我們可以為x86手機(jī)直接提供x86的so文件钧椰,而不是僅提供arm的so讓系統(tǒng)通過houdini去動(dòng)態(tài)轉(zhuǎn)換arm指令,避免轉(zhuǎn)換過程中的性能損耗蜂科。
查看Android系統(tǒng)支持的ABI有以下兩種方法:
使用adb命令
/system/build.prop
中指定了支持的ABI類型栅受,在adb中,可使用如下命令查看:
shell@NX529J:/ $ getprop | grep abilist
[ro.product.cpu.abi]: [arm64-v8a]
[ro.product.cpu.abilist32]: [armeabi-v7a,armeabi]
[ro.product.cpu.abilist64]: [arm64-v8a]
[ro.product.cpu.abilist]: [arm64-v8a,armeabi-v7a,armeabi]
使用API獲取
使用Build.SUPPORTED_ABIS可以獲取當(dāng)前設(shè)備支持的ABI列表:
import android.os.Build;
String supportedAbis = Build.SUPPORTED_ABIS;
x86手機(jī)對arm的支持
值得注意的是原本x86架構(gòu)的CPU是不支持運(yùn)行arm架構(gòu)的native代碼的岩四,但I(xiàn)ntel和Google合作在x86機(jī)子的系統(tǒng)內(nèi)核層之上加入了一個(gè)名為houdini的Binary Translator(二進(jìn)制轉(zhuǎn)換中間層)哭尝,這個(gè)中間層會在運(yùn)行期間動(dòng)態(tài)的讀取arm指令并將之轉(zhuǎn)換為x86指令去執(zhí)行。
所以能看到很多沒有提供x86對應(yīng)so的應(yīng)用(如新浪微博)也能夠運(yùn)行在x86手機(jī)上剖煌。
apk安裝過程中對so的選擇
在Android上安裝應(yīng)用程序時(shí)材鹦,Package Manager會掃描整個(gè)apk文件,尋找符合下面文件路徑格式的動(dòng)態(tài)連接庫:
lib/<primary-abi>/lib<name>.so
在這里耕姊,primary-abi
是上面表中的abi的值桶唐,name
對應(yīng)的是我們在Android.mk中定義的LOCAL_MODULE的值,
如果在apk內(nèi)并沒有找到適合當(dāng)前機(jī)器primary-abi的so箩做,Package Manager會嘗試尋找適合secondary-abi的so文件:
lib/<secondary-abi>/lib<name>.so
即安裝應(yīng)用時(shí)莽红,系統(tǒng)會根據(jù)當(dāng)前CPU架構(gòu)選擇最優(yōu)ABI適配,如果找到了合適的so文件邦邦,包管理器會將該ABI文件夾下所有so庫全部拷貝至應(yīng)用的data目錄下:data/data/<package_name>/lib/
注意:apk安裝過程對so選擇是基于整個(gè)ABI文件夾的安吁,而非以單個(gè)so文件為粒度,也就是說把lib/armeabi 燃辖、lib/armeabi-v7a鬼店、lib/x86等等文件夾的其中一個(gè)文件夾內(nèi)所有.so復(fù)制到應(yīng)用的data目錄下。
如果我們在代碼中調(diào)用了某個(gè)so的功能黔龟,而最終拷貝的ABI文件夾下并沒有提供這個(gè)文件妇智,apk的安裝過程中并不會報(bào)錯(cuò),但是運(yùn)行時(shí)會遇到java.lang.UnsatisfiedLinkError
氏身。
so的加載
對于so的加載巍棱,Android在System
類中提供了兩種方法:
/**
* See {@link Runtime#loadLibrary}.
*/
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
/**
* See {@link Runtime#load}.
*/
public static void load(String pathName) {
Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());
}
System.loadLibrary
這是我們最常用的一個(gè)方法,System.loadLibrary
只需要傳入so在Android.mk中定義的LOCAL_MODULE的值即可蛋欣,
系統(tǒng)會調(diào)用System.mapLibraryName
把這個(gè)libName轉(zhuǎn)化成對應(yīng)平臺的so的全稱并去嘗試尋找這個(gè)so加載航徙。
比如我們的so文件全名為libmath.so,加載該動(dòng)態(tài)庫只需要傳入math
即可:
System.loadLibrary("math");
System.load
對于System.load
方法陷虎,官方是這樣介紹的:
Loads a code file with the specified filename from the local file system as a dynamic library.
The filename argument must be a complete path name.
所以它為動(dòng)態(tài)加載非apk打包期間內(nèi)置的so文件提供了可能到踏,也就是說可以使用這個(gè)方法來指定我們要加載的so文件的路徑來動(dòng)態(tài)的加載so文件杠袱。
比如我們在打包期間并不打包so文件,而是在應(yīng)用運(yùn)行時(shí)將當(dāng)前設(shè)備適用的so文件從服務(wù)器上下載下來窝稿,放在/data/data/<package-name>/mydir
下楣富,然后在使用so時(shí)調(diào)用:
System.load("/data/data/<package-name>/mydir/libmath.so");
即可成功加載這個(gè)so,開始調(diào)用本地方法了伴榔。
其實(shí)loadLibrary和load最終都會調(diào)用nativeLoad(name, loader, ldLibraryPath)方法纹蝴,只是因?yàn)閘oadLibrary的參數(shù)傳入的僅僅是so的文件名,所以踪少,loadLibrary需要首先找到這個(gè)文件的路徑骗灶,然后加載這個(gè)so文件。
而load傳入的參數(shù)是一個(gè)文件路徑秉馏,所以它不需要去尋找這個(gè)文件路徑,而是直接通過這個(gè)路徑來加載so文件脱羡。
但是當(dāng)我們把需要加載的so文件放在SdCard中萝究,會發(fā)生什么呢?把上面so的路徑改成/mnt/sdcard/libmath.so
锉罐,再嘗試加載時(shí)帆竹,會得到如下錯(cuò)誤:
java.lang.UnsatisfiedLinkError: dlopen failed: couldn't map "/mnt/sdcard/libmath.so" segment 1: Permission denied
這是因?yàn)镾D卡等外部存儲路徑是一種可拆卸的(mounted)不可執(zhí)行(noexec)的儲存媒介,不能直接用來作為可執(zhí)行文件的運(yùn)行目錄脓规,使用前應(yīng)該把可執(zhí)行文件復(fù)制到APP內(nèi)部存儲下再運(yùn)行栽连。所以使用System.load
加載so時(shí)要注意把so拷貝至/data/data/<package-name>/
下。
通過精簡so來減小包大小
現(xiàn)在的apk動(dòng)輒幾十M或者更大侨舆,apk包大小的精簡成為了開發(fā)過程中的重要一環(huán)秒紧。通過上面的介紹,我們知道x86挨下、x86_64熔恢、armeabi-v7a、arm64-v8a設(shè)備都支持armeabi架構(gòu)的so臭笆,因此叙淌,通過移除不必要的so來減小包大小是一個(gè)不錯(cuò)的選擇。
按照ABI分別單獨(dú)打包APK
我們可以選擇在Google Play上傳指定ABI版本的APK愁铺,生成不同ABI版本的APK可以在build.gradle中進(jìn)行如下配置:
android {
// Some other configuration here...
splits {
abi {
enable true
reset()
include 'x86', 'armeabi', 'armeabi-v7a', 'mips' //select ABIs to build APKs for
universalApk false // generate an additional APK that contains all the ABIs
}
}
}
只提供armabi
的so
上面的方法需要應(yīng)用市場提供用戶設(shè)備CPU類型更識別的支持鹰霍,在國內(nèi)并不是一個(gè)十分適用的方案。常用的處理方式是利用gradle中的abiFilters配置茵乱。
首先配置修改主工程build.gradle
下的abiFilters
:
android {
// Some other configuration here...
defaultConfig {
ndk {
abiFilters 'armeabi'
}
}
}
abiFilters后面的ABI類型即為要打包進(jìn)apk的ABI類型茂洒,除此以外都不打包進(jìn)apk里。
然后在項(xiàng)目的根目錄下的gradle.properties
(沒有的話新建一個(gè))中加入下面這行:
android.useDeprecatedNdk=true
通過上面方法減少的apk體積是十分可觀的似将,也是目前比較主流的處理方案获黔。
進(jìn)階版方案
如果進(jìn)一步蚀苛,會發(fā)現(xiàn)上面的方案并不完美。首先是性能問題:使用兼容模式去運(yùn)行arm架構(gòu)的so玷氏,會丟失專門為當(dāng)前ABI優(yōu)化過的性能堵未;其次還有兼容性問題,雖然x86設(shè)備能兼容arm類型的函數(shù)庫盏触,但是并不意味著100%的兼容渗蟹,某些情況下還是會發(fā)生crash,所以x86的arm兼容只是一個(gè)折中方案赞辩,為了最好的利用x86自身的性能和避免兼容性問題雌芽,我們最好的做法仍是專為x86
提供對應(yīng)的so。
針對這些問題辨嗽,我們可以采用一個(gè)相對更好的方案:讓所有so都來自于網(wǎng)路世落,應(yīng)用下載服務(wù)器上的so庫后,利用System.load
方法動(dòng)態(tài)加載當(dāng)前設(shè)備對應(yīng)的so.
需要注意的問題
不要把so放錯(cuò)地方
首先要注意的是不要把另一個(gè)ABI下的so文件放在另一個(gè)ABI文件夾下(每個(gè)ABI文件夾下的so文件名是相同的糟需,有可能會搞錯(cuò))屉佳。
盡可能為所有ABI提供so
理想狀況下,應(yīng)該盡可能為所有ABI都提供對應(yīng)的so洲押,這一點(diǎn)的好處我們已經(jīng)在上面討論過了:在可以發(fā)揮更好性能的同時(shí)武花,還能減少由于兼容帶來的某些crash問題。當(dāng)然杈帐,這一點(diǎn)要結(jié)合實(shí)際情況(如SDK提供的so不全体箕、芯片市場占有率、apk包大小等)去考量挑童,如果使用的so本身就很小累铅,我們大可為盡可能多的ABI都提供so。
若是局限于包大小等因素站叼,可以結(jié)合通過精簡so來減小包大小一節(jié)中提供的第三個(gè)方案來調(diào)整so的使用策略争群。
所有ABI文件夾提供的so要保持一致
這是一個(gè)十分容易出現(xiàn)的錯(cuò)誤。
如果我們的應(yīng)用選擇了支持多個(gè)ABI大年,要十分注意:對于每個(gè)ABI下的so换薄,但要么全部支持,要么都不支持翔试。不應(yīng)該混合著使用轻要,而應(yīng)該為每個(gè)ABI目錄提供對應(yīng)的.so文件。
先舉個(gè)例子垦缅,Bugtags的so支持所有的ABI:
libs
|
├── arm64-v8a
│ └── libBugtags.so
├── armeabi
│ └── libBugtags.so
├── armeabi-v7a
│ └── libBugtags.so
├── mips
│ └── libBugtags.so
├── mips64
│ └── libBugtags.so
├── x86
│ └── libBugtags.so
└── x86_64
└── libBugtags.so
但不是所有開發(fā)者提供的so都支持所有ABI:
lib
|
├── armeabi
│ └── libImages.so
└── armeabi-v7a
└── libImages.so
如果不做任何設(shè)置冲泥,最終打出來的apk的lib目錄會是這樣的:
lib
|
├── arm64-v8a
│ └── libBugtags.so
├── armeabi
│ ├── libBugtags.so
│ └── libImages.so
├── armeabi-v7a
│ ├── libBugtags.so
│ └── libImages.so
├── mips
│ └── libBugtags.so
├── mips64
│ └── libBugtags.so
├── x86
│ └── libBugtags.so
└── x86_64
└── libBugtags.so
參考上面apk安裝過程中對so的選擇一節(jié),假設(shè)當(dāng)前設(shè)備是x86機(jī)器,包管理器會先去lib/x86下尋找凡恍,發(fā)現(xiàn)該文件夾是存在的志秃,所以最終只有l(wèi)ib/x86下的so--即只有l(wèi)ibBugtags.so會被安裝。當(dāng)嘗試在運(yùn)行期間加載libImages.so
時(shí)嚼酝,就會遇上下面常見的UnsatisfiedLinkError錯(cuò)誤:
E/xxx (10674): java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/xxx-2/base.apk"],nativeLibraryDirectories=[/data/app/xxx-2/lib/x86, /vendor/lib, /system/lib]]] couldn't find "libImages.so"
E/xxx (10674): at java.lang.Runtime.loadLibrary(Runtime.java:366)
所以浮还,我們需要遵循這樣的準(zhǔn)則:
- 對于so開發(fā)者:支持所有的平臺,否則將會搞砸你的用戶闽巩。
- 對于so使用者:要么支持所有平臺钧舌,要么都不支持。
然而涎跨,因?yàn)榉N種原因(遺留so洼冻、芯片市場占有率、apk包大小等)隅很,并不是所有人都遵循這樣的原則撞牢。
一種可行的處理方案是:取你所有的so庫所支持的ABI的交集,移除其他(可以通過上面介紹的abiFilters
來實(shí)現(xiàn))叔营。
如上面的例子普泡,最終生成的apk可以是:
lib
|
├── armeabi
│ ├── libBugtags.so
│ └── libImages.so
└── armeabi-v7a
├── libBugtags.so
└── libImages.so