原文連結:http://allenfeng.com/2016/11/06/what-you-should-know-about-android-abi-and-so/
一般情況下,我們不需要關心so。但是當APP使用的第三方SDK中包含了so檔案,或者自己需要使用NDK開發某些功能,就有必要去好好了解下so的一些知識。
什麼是ABI和so
早期的Android裝置隻支援ARMv5的CPU架構,随着Android系統的快速發展,搭載Android的硬體平台也早已多樣化了,又加入了ARMv7,x86,MIPS,ARMv8,MIPS64和x86_64。
每一種CPU架構,都定義了一種ABI(Application Binary Interface,應用二進制接口),ABI定義了其所對應的CPU架構能夠執行的二進制檔案(如.so檔案)的格式規範,決定了二進制檔案如何與系統進行互動。
每一種ABI的詳細介紹可以參見官方的介紹ABI Management。
so(shared object,共享庫)是機器可以直接運作的二進制代碼,是Android上的動态連結庫,類似于Windows上的dll。每一個Android應用所支援的ABI是由其APK提供的.so檔案決定的,這些so檔案被打包在apk檔案的lib/目錄下,其中abi可以是上面表格中的一個或者多個。
例如,解壓一個apk檔案後,在lib目錄下可以看到如下檔案:
| |
說明該應用所支援的ABI為armeabi, armeabi-v7a, mips, 和x86。
注:可以使用
aapt
指令快速檢視apk支援的abi
| |
為什麼使用so
- so機制讓開發者最大化利用已有的C和C++代碼,達到重用的效果,利用軟體世界積累了幾十年的優秀代碼;
- so是二進制,沒有解釋編譯的開消,用so實作的功能比純java實作的功能要快;
- so記憶體配置設定不受Dalivik/ART的單個應用限制,減少OOM;
- 相對于java代碼,二進制代碼的反編譯難度更大,一些核心代碼可以考慮放在so中。
為指定的ABI生成so
預設情況下,NDK隻會為armeabi生成.so檔案,若需要生成支援其他ABI的.so檔案,可以在Application.mk檔案中指定
APP_ABI
參數:
| |
APP_ABI
參數可以被指定多個值以支援多個ABI:
| |
當然,你也可以使用
all
來生成支援所有ABI的so:
| |
檢視Android系統的ABI支援
Android可以在運作期間确定目前系統所支援的ABI,這是由系統編譯時的具體參數指定的:
-
(主ABI):對應目前系統中使用的機器碼類型primary ABI
-
(副ABI):表示目前系統支援的其他ABI類型secondary ABI
許多手機支援不止一個ABI,比如,一個基于ARMv7的裝置會将armeabi-v7a定義為primary ABI,armeabi作為secondary ABI,意味着這台機器同時支援armeabi-v7a和armeabi。
許多基于x86的裝置也可以運作armeabi-v7a和armeabi的so,對于這些機器,primary ABI是x86,secondary ABI則是armeabi-v7a.
但是,為了能得到更好的性能表現,我們應該盡可能的直接提供primary ABI所對應的so檔案。比如,我們可以為x86手機直接提供x86的so檔案,而不是僅提供arm的so讓系統通過houdini去動态轉換arm指令,避免轉換過程中的性能損耗。
檢視Android系統支援的ABI有以下兩種方法:
使用adb指令
/system/build.prop
中指定了支援的ABI類型,在adb中,可使用如下指令檢視:
| |
使用API擷取
使用Build.SUPPORTED_ABIS可以擷取目前裝置支援的ABI清單:
| |
x86手機對arm的支援
值得注意的是原本x86架構的CPU是不支援運作arm架構的native代碼的,但Intel和Google合作在x86機子的系統核心層之上加入了一個名為houdini的Binary Translator(二進制轉換中間層),這個中間層會在運作期間動态的讀取arm指令并将之轉換為x86指令去執行。
是以能看到很多沒有提供x86對應so的應用(如新浪微網誌)也能夠運作在x86手機上。
apk安裝過程中對so的選擇
在Android上安裝應用程式時,Package Manager會掃描整個apk檔案,尋找符合下面檔案路徑格式的動态連接配接庫:
| |
在這裡,
primary-abi
是上面表中的abi的值,
name
對應的是我們在Android.mk中定義的LOCAL_MODULE的值,
如果在apk内并沒有找到适合目前機器primary-abi的so,Package Manager會嘗試尋找适合secondary-abi的so檔案:
| |
即安裝應用時,系統會根據目前CPU架構選擇最優ABI适配,如果找到了合适的so檔案,包管理器會将該ABI檔案夾下所有so庫全部拷貝至應用的data目錄下:
data/data/<package_name>/lib/
注意:apk安裝過程對so選擇是基于整個ABI檔案夾的,而非以單個so檔案為粒度,也就是說把lib/armeabi 、lib/armeabi-v7a、lib/x86等等檔案夾的其中一個檔案夾内所有.so複制到應用的data目錄下。
如果我們在代碼中調用了某個so的功能,而最終拷貝的ABI檔案夾下并沒有提供這個檔案,apk的安裝過程中并不會報錯,但是運作時會遇到
java.lang.UnsatisfiedLinkError
。
so的加載
對于so的加載,Android在
System
類中提供了兩種方法:
| |
System.loadLibrary
這是我們最常用的一個方法,
System.loadLibrary
隻需要傳入so在Android.mk中定義的LOCAL_MODULE的值即可,
系統會調用
System.mapLibraryName
把這個libName轉化成對應平台的so的全稱并去嘗試尋找這個so加載。
比如我們的so檔案全名為libmath.so,加載該動态庫隻需要傳入
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.
是以它為動态加載非apk打包期間内置的so檔案提供了可能,也就是說可以使用這個方法來指定我們要加載的so檔案的路徑來動态的加載so檔案。
比如我們在打包期間并不打包so檔案,而是在應用運作時将目前裝置适用的so檔案從伺服器上下載下傳下來,放在
/data/data/<package-name>/mydir
下,然後在使用so時調用:
| |
即可成功加載這個so,開始調用本地方法了。
其實loadLibrary和load最終都會調用nativeLoad(name, loader, ldLibraryPath)方法,隻是因為loadLibrary的參數傳入的僅僅是so的檔案名,是以,loadLibrary需要首先找到這個檔案的路徑,然後加載這個so檔案。
而load傳入的參數是一個檔案路徑,是以它不需要去尋找這個檔案路徑,而是直接通過這個路徑來加載so檔案。
但是當我們把需要加載的so檔案放在SdCard中,會發生什麼呢?把上面so的路徑改成
/mnt/sdcard/libmath.so
,再嘗試加載時,會得到如下錯誤:
| |
這是因為SD卡等外部存儲路徑是一種可拆卸的(mounted)不可執行(noexec)的儲存媒介,不能直接用來作為可執行檔案的運作目錄,使用前應該把可執行檔案複制到APP内部存儲下再運作。是以使用
System.load
加載so時要注意把so拷貝至
/data/data/<package-name>/
下。
通過精簡so來減小包大小
現在的apk動辄幾十M或者更大,apk包大小的精簡成為了開發過程中的重要一環。通過上面的介紹,我們知道x86、x86_64、armeabi-v7a、arm64-v8a裝置都支援armeabi架構的so,是以,通過移除不必要的so來減小包大小是一個不錯的選擇。
按照ABI分别單獨打包APK
我們可以選擇在Google Play上傳指定ABI版本的APK,生成不同ABI版本的APK可以在build.gradle中進行如下配置:
| |
隻提供 armabi
的so
armabi
上面的方法需要應用市場提供使用者裝置CPU類型更識别的支援,在國内并不是一個十分适用的方案。常用的處理方式是利用gradle中的abiFilters配置。
首先配置修改主工程
build.gradle
下的
abiFilters
:
| |
abiFilters後面的ABI類型即為要打包進apk的ABI類型,除此以外都不打包進apk裡。
然後在項目的根目錄下的
gradle.properties
(沒有的話建立一個)中加入下面這行:
| |
通過上面方法減少的apk體積是十分可觀的,也是目前比較主流的處理方案。
進階版方案
如果進一步,會發現上面的方案并不完美。首先是性能問題:使用相容模式去運作arm架構的so,會丢失專門為目前ABI優化過的性能;其次還有相容性問題,雖然x86裝置能相容arm類型的函數庫,但是并不意味着100%的相容,某些情況下還是會發生crash,是以x86的arm相容隻是一個折中方案,為了最好的利用x86自身的性能和避免相容性問題,我們最好的做法仍是專為
x86
提供對應的so。
針對這些問題,我們可以采用一個相對更好的方案:讓所有so都來自于網路,應用下載下傳伺服器上的so庫後,利用
System.load
方法動态加載目前裝置對應的so.
需要注意的問題
不要把so放錯地方
首先要注意的是不要把另一個ABI下的so檔案放在另一個ABI檔案夾下(每個ABI檔案夾下的so檔案名是相同的,有可能會搞錯)。
盡可能為所有ABI提供so
理想狀況下,應該盡可能為所有ABI都提供對應的so,這一點的好處我們已經在上面讨論過了:在可以發揮更好性能的同時,還能減少由于相容帶來的某些crash問題。當然,這一點要結合實際情況(如SDK提供的so不全、晶片市場占有率、apk包大小等)去考量,如果使用的so本身就很小,我們大可為盡可能多的ABI都提供so。
若是局限于包大小等因素,可以結合通過精簡so來減小包大小一節中提供的第三個方案來調整so的使用政策。
所有ABI檔案夾提供的so要保持一緻
這是一個十分容易出現的錯誤。
如果我們的應用選擇了支援多個ABI,要十分注意:對于每個ABI下的so,但要麼全部支援,要麼都不支援。不應該混合着使用,而應該為每個ABI目錄提供對應的.so檔案。
先舉個例子,Bugtags的so支援所有的ABI:
| |
但不是所有開發者提供的so都支援所有ABI:
| |
如果不做任何設定,最終打出來的apk的lib目錄會是這樣的:
| |
參考上面apk安裝過程中對so的選擇一節,假設目前裝置是x86機器,包管理器會先去lib/x86下尋找,發現該檔案夾是存在的,是以最終隻有lib/x86下的so–即隻有libBugtags.so會被安裝。當嘗試在運作期間加載
libImages.so
時,就會遇上下面常見的UnsatisfiedLinkError錯誤:
| |
是以,我們需要遵循這樣的準則:
- 對于so開發者:支援所有的平台,否則将會搞砸你的使用者。
- 對于so使用者:要麼支援所有平台,要麼都不支援。
然而,因為種種原因(遺留so、晶片市場占有率、apk包大小等),并不是所有人都遵循這樣的原則。
一種可行的處理方案是:取你所有的so庫所支援的ABI的交集,移除其他(可以通過上面介紹的
abiFilters
來實作)。
如上面的例子,最終生成的apk可以是:
| |