系統(tǒng)應(yīng)用集成過(guò)程中的一些坑

本篇文章已授權(quán)微信公眾號(hào) dasu_Android(大蘇)獨(dú)家發(fā)布

這次想來(lái)講講系統(tǒng)應(yīng)用集成過(guò)程中遇到的一些坑,尤其是 so 文件相關(guān)的坑。

背景

埋這些坑的最初來(lái)源是由于測(cè)試人員在集成新終端設(shè)備時(shí)提了個(gè) bug: app 在這個(gè)設(shè)備上無(wú)法啟動(dòng)。

隨后拋來(lái)了一份日志,過(guò)濾了下,最重要的其實(shí)就一條,crash 日志:

java.lang.UnsatisfiedLinkError: No implementation found for long com.facebook.imagepipeline.memory.NativeMemoryChunk.nativeAllocate(int) (tried Java_com_facebook_imagepipeline_memory_NativeMemoryChunk_nativeAllocate and Java_com_facebook_imagepipeline_memory_NativeMemoryChunk_nativeAllocate__I)

app 使用了 fresco 圖片庫(kù),最初猜想是不是因?yàn)?so 文件沒(méi)有 push,因?yàn)槲覀兊?app 是系統(tǒng)應(yīng)用,集成的時(shí)候是直接將 apk push 到 system/app 下的,因?yàn)闆](méi)有 install 過(guò)程,所以 so 文件得自己 push 到 system/lib 下。

但把機(jī)子拿過(guò)來(lái)一看,so 文件有在啊,嘗試將其刪掉,再運(yùn)行,又報(bào)出了如下異常:

java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/system/app/xxxx.apk"],nativeLibraryDirectories=[/system/lib64/xxxx, /vendor/lib64, /system/lib64]]] couldn't find "libimagepipeline.so"

看了下日志,它是說(shuō),在 system/lib64 目錄下沒(méi)有找到 so 文件,奇怪,以前都是只集成到 system/lib 下就可以了啊,怎么這次多出了個(gè) system/lib64,難道這個(gè)機(jī)子支持的 CPUABI 不一樣?試著運(yùn)行了下 getprop | get cpu

cpu.png

果然,這個(gè)機(jī)子支持的 CPUABI 多了個(gè) arm64-v8a。

那這個(gè)機(jī)子既支持 arm64-v8a,又支持 armeabi-v7a,我怎么知道,我的 app 該將 so 文件集成在哪里,什么場(chǎng)景該放 system/lib 下,什么時(shí)候該集成到 system/lib64 中?還是說(shuō),兩個(gè)地方都放?

應(yīng)該不至于兩個(gè)目錄都得集成,因?yàn)槿綉?yīng)用安裝時(shí),從 apk 包中也只會(huì)解壓一份 so 文件而已,并不會(huì)將 lib 下所有 abi 架構(gòu)的 so 文件都解壓。

后來(lái),試著查找相關(guān)資料,發(fā)現(xiàn)可以在 data/system/packages.xml 文件中找到自己 app 的相關(guān)配置信息,這里有明確指出該去哪里加載 so 文件,以及 app 所運(yùn)行的 CPU 架構(gòu),所以我們可以運(yùn)行如下命令:

cat /data/system/packages.xml | grep {你自己app的包名}
packages.png

后來(lái)有些疑惑,這里的 primaryCpuAbi 屬性值,系統(tǒng)是如何確定的,因?yàn)橛龅竭^(guò),明明這次的值是 armeabi-v7a,但當(dāng)重啟之后,有時(shí)候居然變成 arm64-v8a 了,所以就又去查找了相關(guān)資料,發(fā)現(xiàn),這個(gè)值確定的流程蠻復(fù)雜的,影響因素也很多。

那么,就沒(méi)有辦法根據(jù)某些條件確定某個(gè)場(chǎng)景來(lái)確定 so 文件是該放 system/lib,還是 system/lib64 了,只能兩個(gè)都集成了。于是乎,嘗試著直接將 system/lib 下的 so 文件拷貝了一份到 system/lib64,結(jié)果發(fā)現(xiàn)運(yùn)行報(bào)了如下異常:

java.lang.UnsatisfiedLinkError: dlopen failed: "libimagepipeline.so" is 32-bit instead of 64-bit

哎,想當(dāng)然了,不同 CPU 架構(gòu)的 so 文件肯定不一樣,哪里可以直接將 armeabi-v7a 的 so 文件放到 system/lib64 里。因此,重新編譯、打包了一份 arm64-v8a 架構(gòu)的 so 文件,集成到 system/lib64 下,再運(yùn)行,搞定。

但你以為事情到這里就結(jié)束了嗎?年輕人,too yang.

由于以前 app 合作的機(jī)子,都只有 armabi-v7a 的,所以集成方式就一種,只需要集成到 system/lib 下就可以了,但由于新合作的機(jī)子有 arm64-v8a 的了,那么此時(shí)就需要修改以前的集成方式,分別將對(duì)應(yīng)的 so 文件集成到對(duì)應(yīng)的 system/lib 和 system/lib64 目錄下。

但運(yùn)維人員表示說(shuō),他不懂這些,他怎么判斷說(shuō),什么時(shí)候該用舊的集成方式,什么時(shí)候用新的集成方式。我跟他說(shuō),你需要先執(zhí)行 getprop | grep cpu 命令,查看當(dāng)前機(jī)子支持的 CPUABI,然后再來(lái)決定你如何集成。但運(yùn)維又說(shuō),這好復(fù)雜,能否有方法就統(tǒng)一一種集成方式,不必分場(chǎng)景考慮。

emmm,你們都是老大,你們說(shuō)了算。只能又去瞎搞了,這次去開(kāi)源庫(kù)的 issue 里嘗試尋找了下,結(jié)果發(fā)現(xiàn),哈哈哈,原來(lái)這么多人碰到過(guò)這個(gè)問(wèn)題:

issus.png

要相信,你絕對(duì)不是第一個(gè)遇到問(wèn)題的人。是吧,這么多人都來(lái)這里提問(wèn)了,開(kāi)源庫(kù)的負(fù)責(zé)人肯定給出解決方案了,所以接下去繼續(xù)在這些 issues 里過(guò)濾一下,找出那些跟你一樣的問(wèn)題就可以了。如下面這篇:

java.lang.UnsatisfiedLinkError #1552

issue.png

官方人員已經(jīng)說(shuō)了,可以嘗試使用 Relinker 或 SoLoader 來(lái)解決。

最后,我選擇了 ReLinker,發(fā)現(xiàn)它的源碼并不多,直接將所有源碼拷貝到項(xiàng)目中,修改了源碼中某個(gè)流程的邏輯,用于解決我自己這種場(chǎng)景下的 so 文件加載問(wèn)題,搞定,具體在下面的埋坑一節(jié)講述。

這整個(gè)過(guò)程中,遇到了一個(gè)又一個(gè)問(wèn)題,一個(gè)又一個(gè)坑,解決這個(gè)異常,出現(xiàn)另一個(gè)異常,但整個(gè)過(guò)程梳理過(guò)來(lái),也掌握了很多干貨知識(shí)點(diǎn),下面就用自己的理解,將這些相關(guān)的知識(shí)點(diǎn)梳理一下:

知識(shí)點(diǎn)

看完本篇,你能了解到哪些知識(shí)點(diǎn)呢,如下:

P1:了解系統(tǒng)應(yīng)用集成方式,大概清楚 apk 的 install 過(guò)程都做了些什么。

P2:知道如何判斷系統(tǒng)應(yīng)用是否安裝成功,懂得查看 data/system/packages.xml 文件來(lái)得知應(yīng)用的基礎(chǔ)信息,如 so 庫(kù)地址,primaryCpuAbi 等。

P3:掌握 System.load() 和 System.loadlibrary() 的區(qū)別。

P4:清楚系統(tǒng)尋找 so 文件的大體流程,知道系統(tǒng)什么時(shí)候會(huì)去 system/lib 下加載 so 文件,什么時(shí)候去 system/lib64。

P5:了解 ReLinker 和 SoLoder 庫(kù)的用途和大體原理。

正文

ps: 由于接觸尚淺,還看不懂源碼,正文部分大多數(shù)是直接從各大神博客中梳理出的結(jié)論,再用以自己的理解表達(dá)出來(lái),因?yàn)椴](méi)有結(jié)合源碼來(lái)分析,因此給出的結(jié)論觀點(diǎn)不保證百分百正確,如有錯(cuò)誤,歡迎指點(diǎn)一下。

ps: 以下知識(shí)點(diǎn)梳理基于的設(shè)備系統(tǒng) Android 5.1.1,api 22,不同系統(tǒng)的設(shè)備,也許過(guò)程會(huì)有些許差別。

1. install 過(guò)程

要了解 apk 的 install 過(guò)程都干了哪些事,先要清楚一個(gè) apk 文件中都有哪些東西,其實(shí) apk 文件就是一個(gè)壓縮包,后綴改為 zip 就可以直接打開(kāi)查看內(nèi)容了,或者 Android Studio 的 Analyze APK 功能也可以查看:

apk結(jié)構(gòu).png

classes.dex 是源代碼,到時(shí)候要加載進(jìn)內(nèi)存運(yùn)行在機(jī)器上的;lib 是存放 so 文件;res 是存放資源文件,包括布局文件、圖片資源等等;assert 同樣存放一些資源文件;AndroidManifest.xml 是清單配置文件;

既然 apk 其實(shí)就是個(gè)壓縮包,將程序運(yùn)行所需要的東西,比如源代碼,比如資源文件等等都打包在一起。那么,install 的過(guò)程,其實(shí)也就是解壓&拷貝&解析的過(guò)程。

但不管是哪個(gè)過(guò)程,首先,這個(gè) apk 文件得先傳送到終端設(shè)備上,所以,我們開(kāi)發(fā)期間使用的 adb install 命令,或者是直接點(diǎn)擊 AndroidStudio 的 run 圖標(biāo)指令(本質(zhì)上也是運(yùn)行的 adb install),這個(gè)命令其實(shí)就干了兩件事:

  1. adb push
  2. pm install

先將 apk push 到終端設(shè)備的臨時(shí)目錄,大多數(shù)場(chǎng)景下是:data/local/tmp

adbinstall.png

如果你有注意執(zhí)行完 adb install 命令后,會(huì)先有一個(gè)百分比的進(jìn)度,這個(gè)進(jìn)度其實(shí)并不是安裝的進(jìn)度,而是 adb push 的進(jìn)度,你可以試著直接執(zhí)行 adb push 命令,看一下是否會(huì)有一個(gè)進(jìn)度提示。

先將 apk 從電腦上 push 到終端設(shè)備,然后調(diào)用 pm install 命令來(lái)安裝 apk。

調(diào)用了 pm install 命令后就會(huì)通知系統(tǒng)去安裝這個(gè) apk 了,也就是上述說(shuō)的拷貝、解壓、解析這幾個(gè)過(guò)程。

拷貝是因?yàn)椋娣旁?data/local/tmp 下的 apk 文件始終是位于臨時(shí)目錄下,隨時(shí)可能被刪掉,系統(tǒng)會(huì)先將這個(gè) apk 拷貝一份到 data/app 目錄下。所以,在 data/app 這個(gè)目錄下,你基本可以看到所有三方 app 的 apk 包,如果三方 app 都沒(méi)有另外指定安裝到 SD 卡的話。

拷貝結(jié)束后,就是對(duì)這個(gè) apk 文件進(jìn)行解壓操作,獲取里面的文件,將相關(guān)文件解壓到指定目錄,如:

  • 創(chuàng)建 data/data/{包名} 目錄,存放應(yīng)用運(yùn)行期間所需的數(shù)據(jù)
  • 掃描 apk 包中 lib 目錄的 so 文件結(jié)構(gòu),解壓到應(yīng)用自身存放 so 庫(kù)的目錄,不同版本系統(tǒng)路徑有些不同,我設(shè)備的版本是 android 5.1.1,api 22,三方應(yīng)用的 so 文件存放目錄就在 data/app/{包名}-1/lib 下
  • class.dex 源代碼轉(zhuǎn)換成 odex 格式,緩存到 data/dalvik-cache 目錄下,加快應(yīng)用的代碼運(yùn)行效率
  • 解析 AndroidManifext.xml 文件以及其他相關(guān)文件,將 app 的相關(guān)信息寫(xiě)入 data/system/packages.xml 注冊(cè)表中
  • 還有其他我不清楚的安裝工作

梳理一下,安裝 apk 過(guò)程中,就是解析 apk 中的內(nèi)容,然后將不同作用的文件拷貝到指定目錄中待用,涉及的目錄有:

  • data/data/{包名}
  • data/dalvik-cache
  • data/app/{包名}-1/lib (后綴有可能是 -1,-2)
  • data/system/packages.xml

我沒(méi)有找到存放 res,assert 這些資源文件的目錄,所以我猜想,這些資源文件其實(shí)并沒(méi)有解壓出來(lái),仍舊是存放在 apk 中。我是這么猜想的:

應(yīng)用運(yùn)行期間,類(lèi)加載器所需的源代碼是從 data/dalvik-cache 緩存中加載,如果這里沒(méi)有緩存,則去 data/app/ 對(duì)應(yīng)的 apk 中解壓拿到 class.dex,轉(zhuǎn)換成 odex,再次緩存到 data/dalvik-cache,然后讓類(lèi)加載器去加載。

而代碼運(yùn)行期間所需的數(shù)據(jù)庫(kù)數(shù)據(jù),xml 數(shù)據(jù)等則直接從 data/data/{包名} 中讀取,如果代碼需要 res 或 assert 資源文件,則再去 data/app 下的 apk 中拿取。

這是我的猜想,這也才能解釋?zhuān)瑸槭裁匆坏?data/app 下的 apk 刪掉,應(yīng)用就無(wú)法運(yùn)行,而如果將 data/data/{包名} 以及 data/dalvik-cache 緩存的 odex 源代碼文件刪掉,應(yīng)用仍舊可以照常運(yùn)行。

正確性與否不清楚,只是我的猜想,以后有時(shí)間翻閱源碼驗(yàn)證一下。

小結(jié)一下

一個(gè)三方 apk 的安裝過(guò)程,不管是通過(guò)設(shè)備的有界面交互方式下的安裝,還是沒(méi)有交互界面直接通過(guò) adb install 命令安裝,還是通過(guò) Android Studio 的 run (本質(zhì)上是執(zhí)行 adb install 命令) 安裝。

這個(gè)過(guò)程,首先得先將 apk 文件傳送到終端設(shè)備上,設(shè)備上有了這個(gè) apk 后,系統(tǒng)安裝應(yīng)用的過(guò)程其實(shí)也就是先將這個(gè) apk 拷貝一份到 data/app 目錄下,然后對(duì)其進(jìn)行解壓工作,將 apk 包中的 so 文件解壓出來(lái),將 dex 文件解壓之后對(duì)其進(jìn)行優(yōu)化處理緩存到 data/dalvik-cache 目錄,以便加快之后應(yīng)用的運(yùn)行,最后解析 AndroidManifext.xml 文件,將這個(gè)應(yīng)用的基本信息寫(xiě)入 data/system/packages.xml 文件中,然后創(chuàng)建 data/data/{包名} 目錄供應(yīng)用運(yùn)行期間存放數(shù)據(jù)。

2. 系統(tǒng)應(yīng)用安裝

系統(tǒng)應(yīng)用的安裝方式就不同于三方應(yīng)用了,系統(tǒng)應(yīng)用無(wú)法通過(guò) install 命令來(lái)安裝,其實(shí)也可以說(shuō),adb install 安裝的都是三方應(yīng)用,這些 apk 最后都被安裝到了 data/app 目錄下。

系統(tǒng)應(yīng)用只能是在出 rom 包時(shí)集成,也就是你設(shè)備第一次買(mǎi)來(lái)開(kāi)機(jī)時(shí)就已跟隨著 rom 包自帶的應(yīng)用,除非你的應(yīng)用有 root 權(quán)限。這些應(yīng)用可以升級(jí),但升級(jí)后權(quán)限會(huì)降為三方應(yīng)用,將不在擁有系統(tǒng)權(quán)限,但將升級(jí)后的刪掉,重啟,就又會(huì)恢復(fù)初始版本的系統(tǒng)應(yīng)用了。

這是因?yàn)椋到y(tǒng)應(yīng)用的安裝過(guò)程基本都是在系統(tǒng)啟動(dòng)時(shí)才去進(jìn)行的。

常見(jiàn)的集成方式是直接將 apk 手動(dòng) push 到 system/app 目錄下,同時(shí)解壓出 apk 里面的 so 文件,手動(dòng)將其 push 到 system/lib 下(大部分場(chǎng)景,有的需要 push 到 system/lib64)。

當(dāng) push 完成時(shí),如果是首次 push,那么 data/system/packages.xml 注冊(cè)表中是沒(méi)有這個(gè)系統(tǒng)應(yīng)用的任何信息的,所以需要重啟一下,才能夠運(yùn)行這個(gè)應(yīng)用。

系統(tǒng)在重啟的時(shí)候,會(huì)去掃描 system/app 目錄下的 apk 文件,如果發(fā)現(xiàn)這個(gè) apk 沒(méi)有安裝,那么會(huì)去觸發(fā)它的安裝工作。這也是為什么重啟有時(shí)候會(huì)很耗時(shí),尤其是升級(jí)完 rom 包后,因?yàn)榇藭r(shí)需要安裝一些 apk。

而安裝過(guò)程基本跟三方應(yīng)用一樣,只是因?yàn)?apk 已經(jīng)在 system/app,所以不會(huì)將 apk 拷貝到 data/app。其余的,優(yōu)化 class.dex 格式為 odex 源代碼文件緩存到 data/dalvik-cache,寫(xiě)配置到 data/system/packages.xml 中等等過(guò)程仍舊一樣。

但有一點(diǎn),三方應(yīng)用的 so 文件是直接解壓到 data/app 目錄下,但系統(tǒng)應(yīng)用已不存在于 data/app 了,所以它并沒(méi)有解壓 so 文件這個(gè)過(guò)程,如果 apk 中有使用到 so 文件,那么需要自己手動(dòng) push 到 system/lib 或者 system/lib64 目錄下。

當(dāng)然,也可以另外一種集成方式:

  • apk push 到 system/app/{自己創(chuàng)建的目錄}/
  • so 文件 push 到 system/app/{自己創(chuàng)建的目錄}/lib 中

這種方式的說(shuō)明,請(qǐng)看后面的后記一章節(jié)。

3. packages.xml

這份配置文件在 data/system/ 目錄下,不要小看這份文件,因?yàn)椴还芟到y(tǒng)應(yīng)用還是三方應(yīng)用,安裝過(guò)程中都會(huì)將其自身的基本信息寫(xiě)入這份文件中。所以,借助這份文件,可以獲取到蠻多信息的。

比如,一般排查系統(tǒng)應(yīng)用為什么啟動(dòng)不了,就可以借助這份文件。

碰到過(guò)這么一個(gè)問(wèn)題,我們做的一些應(yīng)用是沒(méi)有界面的,就純粹在后臺(tái)干活。如果是三方,也許還可以通過(guò)手動(dòng)去啟動(dòng)這個(gè)應(yīng)用來(lái)查看相關(guān)日志,但偏偏還有些應(yīng)用是設(shè)備開(kāi)機(jī)時(shí)就自啟的,所以最怕遇到的問(wèn)題就是測(cè)試人員跟你說(shuō),這個(gè)應(yīng)用在某個(gè)終端上起不來(lái)。

因?yàn)檫@時(shí),不清楚這個(gè)應(yīng)用到底是不是因?yàn)榇a問(wèn)題導(dǎo)致一直崩潰,起不來(lái);還是因?yàn)楦揪蜎](méi)安裝成功;所以,遇到這類(lèi)問(wèn)題,第一點(diǎn)就是要先確認(rèn)這一點(diǎn),而確認(rèn)這一點(diǎn),就可以借助 packages.xml 這份配置文件了。

如果能夠在這份 packages.xml 配置文件中找到應(yīng)用的信息,那么說(shuō)明安裝成功了,接下去就往另一個(gè)方向排查問(wèn)題了。

還有一種場(chǎng)景借助這份配置文件分析也是很有幫助的。

我們還遇到這種情況:

首先 system/app 下是系統(tǒng)應(yīng)用,data/app 下是三方應(yīng)用,但系統(tǒng)是允許 system/app 和 data/app 下存在相同包名的應(yīng)用,因?yàn)樵试S系統(tǒng)應(yīng)用進(jìn)行升級(jí)操作,只是此時(shí)系統(tǒng)應(yīng)用將變成三方應(yīng)用權(quán)限。

某次,有反饋說(shuō),system/app 下已集成了最新版本的應(yīng)用,但為什么,每次啟動(dòng)應(yīng)用時(shí),運(yùn)行的都是舊版本。這時(shí)候怎么排查,就是根據(jù) packages.xml 中這個(gè)應(yīng)用的基本信息,它包括,這個(gè)應(yīng)用的版本號(hào),apk 的來(lái)源目錄,so 文件的加載地址,所申請(qǐng)的權(quán)限等等。

有了這些信息,足夠確認(rèn),此刻運(yùn)行的應(yīng)用是 data/app 下的 apk,還是 system/app 下的 apk。確認(rèn)了之后,再進(jìn)一步去排查。

4. System.load 和 System.loadlibrary

load()loadlibrary() 都是用來(lái)加載 so 文件的,區(qū)別僅在于 load() 接收的是絕對(duì)路徑,比如 ”data/data/{包名}/lib/xxx.so“ 這樣子,因?yàn)槭墙^對(duì)路徑,所以最后跟著的是 so 文件全名,包括后綴名。

loadlibrary() 只需傳入 so 文件去頭截尾的名字就可以了,如 libblur.so,只需傳入 blur 即可,內(nèi)部會(huì)自動(dòng)補(bǔ)全 so 文件可能存在的路徑,以及補(bǔ)全 lib 前綴和 .so 后綴。

所以,下面要講的,其實(shí)就是 loadlibrary() 加載 so 文件的流程。

因?yàn)橹芭龅竭^(guò)這么個(gè)問(wèn)題,有些不大理解:

我們的應(yīng)用是系統(tǒng)應(yīng)用,那么 so 文件也就是集成到 system/lib 或者 system/lib64 目錄下,但不清楚,程序是根據(jù)什么決定是應(yīng)該去 system/lib 目錄下加載 so 文件,還是去 system/lib64 下加載,或者兩處都會(huì)去?

所以,下個(gè)小節(jié)就是講這個(gè)。

5. so 文件加載流程

這節(jié)是本篇的重點(diǎn),打算親自過(guò)下源碼來(lái)梳理,但這樣篇幅會(huì)特別長(zhǎng),基于此,就另起一篇來(lái)專(zhuān)門(mén)寫(xiě)從源碼中梳理 so 文件的加載流程吧,這里就只給出鏈接和幾點(diǎn)結(jié)論,感興趣的可以去看看。

Android 的 so 文件加載機(jī)制

  • 一個(gè)應(yīng)用在安裝過(guò)程中,系統(tǒng)會(huì)經(jīng)過(guò)一系列復(fù)雜的邏輯確定兩個(gè)跟 so 文件加載相關(guān)的 app 屬性值:nativeLibraryDirectories ,primaryCpuAbi ;
  • nativeLibraryDirectories 表示應(yīng)用自身存放 so 文件的目錄地址,影響著 so 文件的加載流程;
  • primaryCpuAbi 表示應(yīng)用應(yīng)該運(yùn)行在哪種 abi 上,如(armeabi-v7a),它影響著應(yīng)用是運(yùn)行在 32 位還是 64 位的進(jìn)程上,進(jìn)而影響到尋找系統(tǒng)指定的 so 文件目錄的流程;
  • 以上兩個(gè)屬性,在應(yīng)用安裝結(jié)束后,可在 data/system/packages.xml 中查看;
  • 當(dāng)調(diào)用 System 的 loadLibrary() 加載 so 文件時(shí),流程如下:
  • 先到 nativeLibraryDirectories 指向的目錄中尋找,是否存在且可用的 so 文件,有則直接加載這里的 so 文件;
  • 上一步?jīng)]找到的話,則根據(jù)當(dāng)前進(jìn)程如果是 32 位的,那么依次去 vendor/lib 和 system/lib 目錄中尋找;
  • 同樣,如果當(dāng)前進(jìn)程是 64 位的,那么依次去 vendor/lib64 和 system/lib64 目錄中尋找;
  • 當(dāng)前應(yīng)用是運(yùn)行在 32 位還是 64 位的進(jìn)程上,取決于系統(tǒng)的 ro.zygote 屬性和應(yīng)用的 primaryCpuAbi 屬性值,系統(tǒng)的 ro.zygote 可通過(guò)執(zhí)行 getprop 命令查看;
  • 如果 ro.zygote 屬性為 zygote64_32,那么應(yīng)用啟動(dòng)時(shí),會(huì)先在 ro.product.cpu.abilist64 列表中尋找是否支持 primaryCpuAbi 屬性,有,則該應(yīng)用運(yùn)行在 64 位的進(jìn)程上;
  • 如果上一步不支持,那么會(huì)在 ro.product.cpu.abilist32 列表中尋找是否支持 primaryCpuAbi 屬性,有,則該應(yīng)用運(yùn)行在 32 位的進(jìn)程上;
  • 如果 ro.zygote 屬性為 zygote32_64,則上述兩個(gè)步驟互換;
  • 如果應(yīng)用的 primaryCpuAbi 屬性為空,那么以 ro.product.cpu.abilist 列表中第一個(gè) abi 值作為應(yīng)用的 primaryCpuAbi;
  • 運(yùn)行在 64 位的 abi 有:arm64-v8a,mips64,x86_64
  • 運(yùn)行在 32 位的 abi 有:armeabi-v7a,armeabi,mips,x86
  • 通常支持 arm64-v8a 的 64 位設(shè)備,都會(huì)向下兼容支持 32 位的 abi 運(yùn)行;
  • 但應(yīng)用運(yùn)行期間,不能混合著使用不同 abi 的 so 文件;
  • 比如,當(dāng)應(yīng)用運(yùn)行在 64 位進(jìn)程中時(shí),無(wú)法使用 32 位 abi 的 so 文件,同樣,應(yīng)用運(yùn)行在 32 位進(jìn)程中時(shí),也無(wú)法使用 64 位 abi 的 so 文件;

6. 三方庫(kù) ReLinker 和 Soloder

ReLinker 和 Soloder 都是用于解決一些 so 文件加載失敗的場(chǎng)景,比如:

  • 嵌套的 so 文件加載異常,如程序引用了三方庫(kù),三方庫(kù)又引用了三方庫(kù),各自庫(kù)中又都存在 so 文件加載,有時(shí)候可能會(huì)導(dǎo)致 so 文件加載失敗。
  • so 文件缺失導(dǎo)致加載異常,如程序的 so 文件在設(shè)備的 so 目錄中不見(jiàn)了之類(lèi)的異常。
  • 等等

它們的 Github 地址:

SoLoader:https://github.com/facebook/SoLoader

ReLinker:https://github.com/KeepSafe/ReLinker

ReLinker 的原理我有去源碼梳理了一遍,大體上是這樣:

  1. 先調(diào)用系統(tǒng) System.loadlibrary() 加載 so 文件,如果成功,結(jié)束;
  2. 如果失敗,則重新解壓 apk 文件,解析其中的 lib 目錄,遍歷 so 文件,找到所需的 so 文件時(shí),將其緩存一份至 data/data/{包名}/app-lib 目錄下,調(diào)用 System.load() 加載這份 so 文件;
  3. 之后每次應(yīng)用重啟,仍舊先調(diào)用系統(tǒng)的 System.loadlibrary() 去嘗試加載 so 文件,失敗,如果 app-lib 下有緩存,且可用,則加載這個(gè)緩存的 so 文件,否則重新解壓 apk,繼續(xù) 2 步驟。
  • 當(dāng)然,解壓 apk 遍歷 so 文件時(shí),如果需要的 so 文件存在于不同的 CPU 架構(gòu)目錄中,并不加以區(qū)分,直接拿第一個(gè)遍歷到的 so 文件。

SoLoder 的原理我只是稍微過(guò)了下,并沒(méi)有詳細(xì)看,因?yàn)槲易詈筮x擇的是 ReLinker 方案,但也可以大體上說(shuō)一說(shuō):

  • 遍歷設(shè)備所有存放 so 文件的目錄,如 system/lib, vendor/lib,緩存其中所有的 so 文件名。
  • 如果系統(tǒng)加載 so 文件失敗時(shí),則從緩存的所有 so 文件名列表中尋找是否有和當(dāng)前要加載的 so 文件一致的,有則直接加載這個(gè) so 文件。

原理大體上應(yīng)該是這樣,感興趣可以自行去看一下。

那么,這兩個(gè) so 文件加載的開(kāi)源庫(kù)有什么用呢?看你是否有遇到過(guò) so 文件加載異常了,我的應(yīng)用場(chǎng)景在埋坑一節(jié)里細(xì)說(shuō)。

埋坑

好了,理論基礎(chǔ)都已經(jīng)有了,那么接下去就是來(lái)埋坑了。

針對(duì)開(kāi)頭所遇到的 bug,其實(shí)原因歸根結(jié)底就是沒(méi)有加載到正確的 so 文件,比如程序需要加載的是 system/lib64 下的 so 文件,但運(yùn)維人員只集成到 system/lib 中;甚至說(shuō),運(yùn)維人員連 so 文件都忘記集成到 system/lib 下了。

另外,運(yùn)維人員希望,可以有一種統(tǒng)一的集成方法,他不需要去考慮是否還要根據(jù)其他條件來(lái)判斷他是否要集成到 system/lib 還是 system/lib64 還是兩者都要。

那么,解決方案其實(shí)有兩種,一是給他一個(gè)新的無(wú)需考慮場(chǎng)景的集成方式;二是代碼層面做適配,動(dòng)態(tài)去加載所缺失的或不能用的 so 文件。

方案一:系統(tǒng)應(yīng)用集成方式

假設(shè)需要集成的應(yīng)用包名:com.dasu.shuai,apk 文件名:dasu.apk

  1. 在 system/app 目錄下新建子目錄,命名能表示那個(gè)應(yīng)用即可,如:dasu
  2. 將 dasu.apk push 到 system/app/dasu/ 目錄下
  3. 在 system/app/dasu 目錄下新建子目錄:lib/arm,這個(gè)命名是固定的,這樣系統(tǒng)才可以識(shí)別
  4. apk 編譯打包時(shí),可以刪掉其他 CPU 架構(gòu)的 so 文件,只保留 armeabi-v7a 即可(根據(jù)你們應(yīng)用的用戶設(shè)備場(chǎng)景為主)
  5. 解壓 apk 文件,取出里面的 lib/armeabi-v7a 下的 so 文件,push 到 system/lib 或 system/app/dasu/lib/arm 都可以
  6. 重啟(如果應(yīng)用首次集成需重啟,否則 packages.xml 中無(wú)該應(yīng)用的任何信息)

以上方案是針對(duì)我們應(yīng)用自己的用戶群場(chǎng)景的集成方式,如果想要通用,最好注意一下步驟 3 和 4,上述的這兩個(gè)步驟目的在于讓系統(tǒng)將該應(yīng)用的 primaryCpuAbi 屬性識(shí)別成 armeabi-v7a,這樣就無(wú)需編譯多個(gè)不同架構(gòu)的 so 文件,集成也只需集成到 system/lib 目錄中即可。

系統(tǒng)在掃描到 lib/arm 有這個(gè)目錄存在時(shí),會(huì)將 app 的 primaryCpuAbi 設(shè)置成 armeabi-v7a,相對(duì)應(yīng)的,如果是 lib/arm64,那么就設(shè)置成 arm64-v8a,這是在 api22 機(jī)子上測(cè)試的結(jié)果。

方案二:代碼適配

清楚了 ReLinker 的原理后,其實(shí)只要修改其中一個(gè)小小的流程即可。當(dāng)系統(tǒng)加載 so 文件異常,ReLinker 接手來(lái)繼續(xù)尋找 so 文件時(shí),進(jìn)行到解壓 apk 包遍歷所有 so 文件時(shí),如果有多個(gè)不同 CPU 架構(gòu)的 so 文件,此時(shí)修改原本的以第一個(gè)遍歷到的 so 文件的邏輯,將其修改成尋找與此時(shí)應(yīng)用的 primaryCpuAbi 一致的架構(gòu)目錄下的 so 文件來(lái)使用。

我是兩種方案都做了,如果運(yùn)維能夠按照正常步驟集成,那么 so 文件加載異常的概率應(yīng)該就不會(huì)大,即使運(yùn)維哪個(gè)步驟操作失誤了,方案二也可以彌補(bǔ)。

后記

本來(lái)以為這樣子的解決方案足夠解決這個(gè)問(wèn)題了,也達(dá)到了運(yùn)維人員的需求了。但沒(méi)想到,事后居然又發(fā)現(xiàn)了新的問(wèn)題:

由于我們是使用 fresco 圖片庫(kù)的,所以我們 app 的 so 文件其實(shí)都是來(lái)自 fresco 的,但沒(méi)想到,合作的廠商它們自己的 app 也是使用的 fresco,然后他們也需要集成 so 文件。

但由于都是作為系統(tǒng)應(yīng)用集成,so 文件都是統(tǒng)一集成在同一個(gè)目錄中,如 system/lib,那么我們使用的 fresco 的 so 文件肯定就跟他們的 so 文件沖突了,因?yàn)槲募家恢拢詈蠹傻臅r(shí)候就只使用他們的 so 文件。

然后,我們使用的 fresco 版本還跟他們不一樣,結(jié)果就導(dǎo)致了,使用他們的 so 文件,我們的 app 運(yùn)行時(shí)仍舊會(huì)報(bào):

java.lang.UnsatisfiedLinkError: No implementation found for long com.facebook.imagepipeline.memory.NativeMemoryChunk.nativeAllocate(int) (tried Java_com_facebook_imagepipeline_memory_NativeMemoryChunk_nativeAllocate and Java_com_facebook_imagepipeline_memory_NativeMemoryChunk_nativeAllocate__I)

那要確認(rèn)不同版本的 fresco 的 so 文件究竟有哪些差異,也只能去期待 fresco 官網(wǎng)是否有給出相關(guān)的文章。一般來(lái)說(shuō),新版本應(yīng)該能兼容舊版本才對(duì),這也就意味著,我們使用的版本其實(shí)比合作方他們新,如果集成時(shí),使用的是我們的 so 文件,雙方應(yīng)該就都沒(méi)問(wèn)題。但跟他們合作一起集成時(shí),如何來(lái)判斷誰(shuí)使用的版本新,誰(shuí)的舊?都不更新的嗎?

畢竟人家是廠商,我們只是需求合作,我們?nèi)鮿?shì),那還是我們自己再來(lái)想解決方案吧。

原本的 ReLinker 方案只能解決 so 文件不存在,加載失敗,或者 so 文件 abi 異常的問(wèn)題,但解決不了,so 文件的版本更新問(wèn)題。

如果真要從代碼層面著手,也不是不行,每次加載 so 文件前,先手動(dòng)去系統(tǒng)的 so 文件目錄中,將即將要加載的 so 文件進(jìn)行一次 md5 計(jì)算,程序中可以保存打包時(shí)使用的 so 文件的 md5 值,兩者相互比較,來(lái)判斷 so 文件對(duì)應(yīng)的代碼版本是否一致。但這樣會(huì)導(dǎo)致正常的流程需要額外處理一些耗時(shí)工作,自行評(píng)估吧。

或者,讓運(yùn)維人員在集成時(shí),干脆不要將 so 文件集成到 system/lib 目錄中,直接集成到 system/app/{新建目錄}/lib/arm/ 目錄下,這樣我們就只使用我們自己的 so 文件,不用去擔(dān)心跟他們共用時(shí),版本差異問(wèn)題了。

參考資料

1.APK文件結(jié)構(gòu)和安裝過(guò)程

2.Android程序包管理(2)--使用adb install執(zhí)行安裝過(guò)程

3.Android 的 so 文件加載機(jī)制


大家好,我是 dasu,歡迎關(guān)注我的公眾號(hào)(dasuAndroidTv),如果你覺(jué)得本篇內(nèi)容有幫助到你,可以轉(zhuǎn)載但記得要關(guān)注,要標(biāo)明原文哦,謝謝支持~


dasuAndroidTv2.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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