談?wù)凙ndroid的so

一般情況下,我們不需要關(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及支持的指令集

每一種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
各種ABI對應(yīng)的值

查看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í)行。

Binary Translator

所以能看到很多沒有提供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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市审编,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌歧匈,老刑警劉巖垒酬,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異件炉,居然都是意外死亡勘究,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門斟冕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來口糕,“玉大人,你說我怎么就攤上這事磕蛇【懊瑁” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵秀撇,是天一觀的道長超棺。 經(jīng)常有香客問我,道長呵燕,這世上最難降的妖魔是什么棠绘? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上氧苍,老公的妹妹穿的比我還像新娘夜矗。我一直安慰自己,他們只是感情好让虐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布紊撕。 她就那樣靜靜地躺著,像睡著了一般澄干。 火紅的嫁衣襯著肌膚如雪逛揩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天麸俘,我揣著相機(jī)與錄音辩稽,去河邊找鬼。 笑死从媚,一個(gè)胖子當(dāng)著我的面吹牛逞泄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拜效,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼喷众,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了紧憾?” 一聲冷哼從身側(cè)響起到千,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赴穗,沒想到半個(gè)月后憔四,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡般眉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年了赵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甸赃。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡柿汛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出埠对,到底是詐尸還是另有隱情络断,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布项玛,位于F島的核電站妓羊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏稍计。R本人自食惡果不足惜躁绸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧净刮,春花似錦剥哑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至暑认,卻和暖如春困介,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蘸际。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工座哩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人粮彤。 一個(gè)月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓根穷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親导坟。 傳聞我的和親對象是個(gè)殘疾皇子屿良,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容