談?wù)凙ndroid的so

一般情況下,我們不需要關(guān)心so。但是當(dāng)APP使用的第三方SDK中包含了so文件,或者自己需要使用NDK開發(fā)某些功能,就有必要去好好了解下so的一些知識(shí)。

出處: Allen's Zone
作者: Allen Feng

什么是ABI和so

早期的Android設(shè)備只支持ARMv5的CPU架構(gòu),隨著Android系統(tǒng)的快速發(fā)展,搭載Android的硬件平臺(tái)也早已多樣化了,又加入了ARMv7,x86,MIPS,ARMv8,MIPS64和x86_64。
每一種CPU架構(gòu),都定義了一種ABI(Application Binary Interface,應(yīng)用二進(jìn)制接口),ABI定義了其所對(duì)應(yīng)的CPU架構(gòu)能夠執(zhí)行的二進(jìn)制文件(如.so文件)的格式規(guī)范,決定了二進(jìn)制文件如何與系統(tǒng)進(jìn)行交互。

ABI及支持的指令集

每一種ABI的詳細(xì)介紹可以參見官方的介紹ABI Management。

so(shared object,共享庫(kù))是機(jī)器可以直接運(yùn)行的二進(jìn)制代碼,是Android上的動(dòng)態(tài)鏈接庫(kù),類似于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;
  • 相對(duì)于java代碼,二進(jìn)制代碼的反編譯難度更大,一些核心代碼可以考慮放在so中。

為指定的ABI生成so

默認(rèn)情況下,NDK只會(huì)為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
各種ABI對(duì)應(yīng)的值

查看Android系統(tǒng)的ABI支持

Android可以在運(yùn)行期間確定當(dāng)前系統(tǒng)所支持的ABI,這是由系統(tǒng)編譯時(shí)的具體參數(shù)指定的:

  • primary ABI(主ABI):對(duì)應(yīng)當(dāng)前系統(tǒng)中使用的機(jī)器碼類型
  • secondary ABI(副ABI):表示當(dāng)前系統(tǒng)支持的其他ABI類型

許多手機(jī)支持不止一個(gè)ABI,比如,一個(gè)基于ARMv7的設(shè)備會(huì)將armeabi-v7a定義為primary ABI,armeabi作為secondary ABI,意味著這臺(tái)機(jī)器同時(shí)支持armeabi-v7a和armeabi。
許多基于x86的設(shè)備也可以運(yùn)行armeabi-v7a和armeabi的so,對(duì)于這些機(jī)器,primary ABI是x86,secondary ABI則是armeabi-v7a.

但是,為了能得到更好的性能表現(xiàn),我們應(yīng)該盡可能的直接提供primary ABI所對(duì)應(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ī)對(duì)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è)中間層會(huì)在運(yùn)行期間動(dòng)態(tài)的讀取arm指令并將之轉(zhuǎn)換為x86指令去執(zhí)行。

Binary Translator

所以能看到很多沒有提供x86對(duì)應(yīng)so的應(yīng)用(如新浪微博)也能夠運(yùn)行在x86手機(jī)上。

apk安裝過程中對(duì)so的選擇

在Android上安裝應(yīng)用程序時(shí),Package Manager會(huì)掃描整個(gè)apk文件,尋找符合下面文件路徑格式的動(dòng)態(tài)連接庫(kù):

lib/<primary-abi>/lib<name>.so

在這里,primary-abi是上面表中的abi的值,name對(duì)應(yīng)的是我們?cè)?em>Android.mk中定義的LOCAL_MODULE的值,

如果在apk內(nèi)并沒有找到適合當(dāng)前機(jī)器primary-abi的so,Package Manager會(huì)嘗試尋找適合secondary-abi的so文件:

lib/<secondary-abi>/lib<name>.so

即安裝應(yīng)用時(shí),系統(tǒng)會(huì)根據(jù)當(dāng)前CPU架構(gòu)選擇最優(yōu)ABI適配,如果找到了合適的so文件,包管理器會(huì)將該ABI文件夾下所有so庫(kù)全部拷貝至應(yīng)用的data目錄下:data/data/<package_name>/lib/

注意:apk安裝過程對(duì)so選擇是基于整個(gè)ABI文件夾的,而非以單個(gè)so文件為粒度,也就是說把lib/armeabi 、lib/armeabi-v7a、lib/x86等等文件夾的其中一個(gè)文件夾內(nèi)所有.so復(fù)制到應(yīng)用的data目錄下。

如果我們?cè)诖a中調(diào)用了某個(gè)so的功能,而最終拷貝的ABI文件夾下并沒有提供這個(gè)文件,apk的安裝過程中并不會(huì)報(bào)錯(cuò),但是運(yùn)行時(shí)會(huì)遇到java.lang.UnsatisfiedLinkError。

so的加載

對(duì)于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)會(huì)調(diào)用System.mapLibraryName把這個(gè)libName轉(zhuǎn)化成對(duì)應(yīng)平臺(tái)的so的全稱并去嘗試尋找這個(gè)so加載。
比如我們的so文件全名為libmath.so,加載該動(dòng)態(tài)庫(kù)只需要傳入math即可:

System.loadLibrary("math");

System.load

對(duì)于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文件。
比如我們?cè)诖虬陂g并不打包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最終都會(huì)調(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中,會(huì)發(fā)生什么呢?把上面so的路徑改成/mnt/sdcard/libmath.so,再嘗試加載時(shí),會(huì)得到如下錯(cuò)誤:

java.lang.UnsatisfiedLinkError: dlopen failed: couldn't map "/mnt/sdcard/libmath.so" segment 1: Permission denied

這是因?yàn)镾D卡等外部存儲(chǔ)路徑是一種可拆卸的(mounted)不可執(zhí)行(noexec)的儲(chǔ)存媒介,不能直接用來作為可執(zhí)行文件的運(yùn)行目錄,使用前應(yīng)該把可執(zhí)行文件復(fù)制到APP內(nèi)部存儲(chǔ)下再運(yùn)行。所以使用System.load加載so時(shí)要注意把so拷貝至/data/data/<package-name>/下。

通過精簡(jiǎn)so來減小包大小

現(xiàn)在的apk動(dòng)輒幾十M或者更大,apk包大小的精簡(jiǎn)成為了開發(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)用市場(chǎng)提供用戶設(shè)備CPU類型更識(shí)別的支持,在國(guó)內(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)一步,會(huì)發(fā)現(xiàn)上面的方案并不完美。首先是性能問題:使用兼容模式去運(yùn)行arm架構(gòu)的so,會(huì)丟失專門為當(dāng)前ABI優(yōu)化過的性能;其次還有兼容性問題,雖然x86設(shè)備能兼容arm類型的函數(shù)庫(kù),但是并不意味著100%的兼容,某些情況下還是會(huì)發(fā)生crash,所以x86的arm兼容只是一個(gè)折中方案,為了最好的利用x86自身的性能和避免兼容性問題,我們最好的做法仍是專為x86提供對(duì)應(yīng)的so。
針對(duì)這些問題,我們可以采用一個(gè)相對(duì)更好的方案:讓所有so都來自于網(wǎng)路,應(yīng)用下載服務(wù)器上的so庫(kù)后,利用System.load方法動(dòng)態(tài)加載當(dāng)前設(shè)備對(duì)應(yīng)的so.

需要注意的問題

不要把so放錯(cuò)地方

首先要注意的是不要把另一個(gè)ABI下的so文件放在另一個(gè)ABI文件夾下(每個(gè)ABI文件夾下的so文件名是相同的,有可能會(huì)搞錯(cuò))。

盡可能為所有ABI提供so

理想狀況下,應(yīng)該盡可能為所有ABI都提供對(duì)應(yīng)的so,這一點(diǎn)的好處我們已經(jīng)在上面討論過了:在可以發(fā)揮更好性能的同時(shí),還能減少由于兼容帶來的某些crash問題。當(dāng)然,這一點(diǎn)要結(jié)合實(shí)際情況(如SDK提供的so不全、芯片市場(chǎng)占有率、apk包大小等)去考量,如果使用的so本身就很小,我們大可為盡可能多的ABI都提供so。
若是局限于包大小等因素,可以結(jié)合通過精簡(jiǎn)so來減小包大小一節(jié)中提供的第三個(gè)方案來調(diào)整so的使用策略。

所有ABI文件夾提供的so要保持一致

這是一個(gè)十分容易出現(xiàn)的錯(cuò)誤。
如果我們的應(yīng)用選擇了支持多個(gè)ABI,要十分注意:對(duì)于每個(gè)ABI下的so,但要么全部支持,要么都不支持。不應(yīng)該混合著使用,而應(yīng)該為每個(gè)ABI目錄提供對(duì)應(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目錄會(huì)是這樣的:

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安裝過程中對(duì)so的選擇一節(jié),假設(shè)當(dāng)前設(shè)備是x86機(jī)器,包管理器會(huì)先去lib/x86下尋找,發(fā)現(xiàn)該文件夾是存在的,所以最終只有l(wèi)ib/x86下的so--即只有l(wèi)ibBugtags.so會(huì)被安裝。當(dāng)嘗試在運(yùn)行期間加載libImages.so時(shí),就會(huì)遇上下面常見的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)則

  • 對(duì)于so開發(fā)者:支持所有的平臺(tái),否則將會(huì)搞砸你的用戶。
  • 對(duì)于so使用者:要么支持所有平臺(tái),要么都不支持。

然而,因?yàn)榉N種原因(遺留so、芯片市場(chǎng)占有率、apk包大小等),并不是所有人都遵循這樣的原則。

一種可行的處理方案是:取你所有的so庫(kù)所支持的ABI的交集,移除其他(可以通過上面介紹的abiFilters來實(shí)現(xiàn))。
如上面的例子,最終生成的apk可以是:

lib
|
├── armeabi
│   ├── libBugtags.so
│   └── libImages.so
└── armeabi-v7a
    ├── libBugtags.so
    └── libImages.so
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容