本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發(fā)布
最近碰到一些 so 文件問題曙咽,順便將相關知識點梳理一下竿拆。
提問
本文的結論是跟著 System.loadlibrary()
一層層源碼走進去嚣崭,個人對其的理解所整理的笨触,那么開始看源碼之前,先來提幾個問題:
Q1:你知道 so 文件的加載流程嗎雹舀?
Q2:設備存放 so 的路徑有 system/lib芦劣,vendor/lib,system/lib64说榆,vendor/lib64虚吟,知道在哪里規(guī)定了這些路徑嗎?清楚哪些場景下系統(tǒng)會去哪個目錄下尋找 so 文件嗎签财?還是說串慰,所有的目錄都會去尋找?
Q3:Zygote 進程是分 32 位和 64 位的唱蒸,那么邦鲫,系統(tǒng)是如何決定某個應用應該運行在 32 位上,還是 64 位上?
Q4:如果程序跑在 64 位的 Zygote 進程上時庆捺,可以使用 32 位的 so 文件么古今,即應用的 primaryCpuAbi 為 arm64-v8a,那么是否可使用 armeabi-v7a 的 so 文件滔以,兼容的嗎捉腥?
Q2,Q3你画,Q4抵碟,這幾個問題都是基于設備支持 64 位的前提下,在舊系統(tǒng)版本中坏匪,只支持 32 位立磁,也就沒這么多疑問需要處理了。
源碼
準備工作
由于這次的源碼會涉及很多 framework 層的代碼剥槐,包括 java 和 c++唱歧,直接在 AndroidStudio 跟進 SDK 的源碼已不足夠查看到相關的代碼了。所以粒竖,此次是借助 Source Insight 軟件颅崩,而源碼來源如下:
https://android.googlesource.com/platform/
我并沒有將所有目錄下載下來,只下載了如下目錄的源碼:
我沒有下載最新版本的代碼蕊苗,而是選擇了 Tags 下的 More 按鈕沿后,然后選擇 tag 為: android-5.1.1 r24 的代碼下載。所以朽砰,此次分析的源碼是基于這個版本尖滚,其余不同版本的代碼可能會有所不一樣,但大體流程應該都是一致的瞧柔。
分析
源碼分析的過程很長很長漆弄,不想看過程的話,你也可以直接跳到末尾看結論造锅,但就會錯失很多細節(jié)的分析了撼唾。
那么下面就開始來過下源碼吧,分析的入口就是跟著 System.loadlibrary()
走 :
//System#loadlibrary()
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
//Runtime#loadLibrary()
void loadLibrary(String libraryName, ClassLoader loader) {
//1. 程序中通過 System.loadlibrary() 方式哥蔚,這個 loader 就不會為空倒谷,流程走這邊
if (loader != null) {
//2. loader.findLibrary() 這是個重點,這個方法用于尋找 so 文件是否存在
String filename = loader.findLibrary(libraryName);
if (filename == null) {
throw new UnsatisfiedLinkError(loader + " couldn't find \"" + System.mapLibraryName(libraryName) + "\"");
}
//3. 如果 so 文件找到糙箍,那么加載它
String error = doLoad(filename, loader);
if (error != null) {
//4. 如果加載失敗渤愁,那么拋異常
throw new UnsatisfiedLinkError(error);
}
return;
}
//1.1 以下代碼的運行場景我不清楚,但有幾個方法可以蠻看一下
//mapLibraryName 用于拼接 so 文件名的前綴:lib深夯,和后綴.so
String filename = System.mapLibraryName(libraryName);
//...省略
//1.2 mLibPaths 存儲著設備存放 so 文件的目錄地址
for (String directory: mLibPaths) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate))
// 1.3 調用 native 層方法加載 so 庫
String error = doLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
lastError = error;
}
}
//...省略
}
所以抖格,其實 System 的 loadlibrary()
是調用的 Runtime 的 loadLibrary()
,不同系統(tǒng)版本,這些代碼是有些許差別的他挎,但不管怎樣筝尾,重點都還是 loadLibrary()
中調用的一些方法,這些方法基本沒變办桨,改變的只是其他代碼的優(yōu)化寫法筹淫。
那么,要理清 so 文件的加載流程呢撞,或者說损姜,要找出系統(tǒng)是去哪些地址加載 so 文件的,就需要梳理清這些方法:
loader.findLibrary()
doLoad()
第一個方法用于尋找 so 文件殊霞,所涉及的整個流程應該都在這個方法里摧阅,如果可以找到,會返回 so 文件的絕對路徑绷蹲,然后交由 doLoad()
去加載棒卷。
java.library.path
但在深入去探索之前,我想先探索另一條分支祝钢,loader 為空的場景比规。loader 什么時候為空,什么時候不為空拦英,我并不清楚蜒什,只是看別人的文章分析時說,程序中通過 System.loadlibrary()
方式加載 so疤估,那么 loader 就不會為空灾常。那,我就信你了铃拇,不然我也不知道去哪分析為不為空的場景钞瀑。
既然程序不會走另一個分支,為什么我還要先來探索它呢锚贱?因為仔戈,第一個分支太不好探索了,先從另一個分支摸索點經(jīng)驗拧廊,而且還發(fā)現(xiàn)了一些感覺可以拿來講講的方法:
System.mapLibraryName()
用于拼接 so 文件名的前綴 lib
,和后綴 .so
晋修。
mLibPaths
在其他版本的源碼中吧碾,可能就沒有這個變量了,直接就是調用一個方法墓卦,但作用都一樣倦春,我們看看這個變量的賦值:
//Runtime.mLibPaths
private final String[] mLibPaths = initLibPaths();
//Runtime#initLibPaths()
private static String[] initLibPaths() {
String javaLibraryPath = System.getProperty("java.library.path");
//...省略
}
最后都是通過調用 System 的 getProperty()
方法,讀取 java.library.path
的屬性值。
也就是說睁本,通過讀取 java.library.path
的系統(tǒng)屬性值尿庐,是可以獲取到設備存放 so 庫的目錄地址的,那么就來看看在哪里有設置這個屬性值進去呢堰。
System 內部有一個類型為 Properties 的靜態(tài)變量抄瑟,不同版本,這個變量名可能不一樣枉疼,但作用也都一樣皮假,用來存儲這些系統(tǒng)屬性值,這樣程序需要的時候骂维,調用 getProperty()
讀取屬性值時其實是來這個靜態(tài)變量中讀取惹资。而變量的初始化地方在類中的 static 代碼塊中:
//System
static {
//...省略
//1.初始化一些不變的系統(tǒng)屬性值
unchangeableSystemProperties = initUnchangeableSystemProperties();
//2.將上述的屬性值以及一些默認的系統(tǒng)屬性值設置到靜態(tài)變量中
systemProperties = createSystemProperties();
//...
}
//System#initUnchangeableSystemProperties()
private static Properties initUnchangeableSystemProperties() {
//...省略一些屬性值設置
p.put("java.vm.vendor", projectName);
p.put("java.vm.version", runtime.vmVersion());
p.put("file.separator", "/");
p.put("line.separator", "\n");
p.put("path.separator", ":");
//...
//1.這里是重點
parsePropertyAssignments(p, specialProperties());
//...
return p;
}
//System#createSystemProperties()
private static Properties createSystemProperties() {
//1.拷貝不可變的一些系統(tǒng)屬性值
Properties p = new PropertiesWithNonOverrideableDefaults(unchangeableSystemProperties);
//2.設置一些默認的屬性值
setDefaultChangeableProperties(p);
return p;
}
//System#setDefaultChangeableProperties()
private static void setDefaultChangeableProperties(Properties p) {
p.put("java.io.tmpdir", "/tmp");
p.put("user.home", "");
}
static 靜態(tài)代碼塊中的代碼其實就是在初始化系統(tǒng)屬性值,分兩個步驟航闺,一個是先設置一些不可變的屬性值褪测,二是設置一些默認的屬性值,然后將這些存儲在靜態(tài)變量中潦刃。
但其實汰扭,不管在哪個方法中,都沒找到有設置 java.library.path
屬性值的代碼福铅,那這個屬性值到底是在哪里設置的呢萝毛?
關鍵點在于設置不可變的屬性時,有調用了一個 native 層的方法:
//System
/**
* Returns an array of "key=value" strings containing information not otherwise
* easily available, such as #defined library versions.
*/
private static native String[] specialProperties();
這方法會返回 key=value 形式的字符串數(shù)組滑黔,然后 parsePropertyAssignments()
方法會去遍歷這些數(shù)組笆包,將這些屬性值填充到存儲系統(tǒng)屬性值的靜態(tài)變量中。
也就是說略荡,在 native 層還會設置一些屬性值庵佣,而 java.library.path
有可能就是在 native 中設置的,那么就跟下去看看吧汛兜。
System 連同包名的全名是:java.lang.System巴粪;那么,通常粥谬,所對應的 native 層的 cpp 文件名為:java_lang_System.cpp肛根,到這里去看看:
//platform/libcore/luni/src/main/native/java_lang_System.cpp#System_specialProperties()
static jobjectArray System_specialProperties(JNIEnv* env, jclass) {
std::vector<std::string> properties;
//...
//1. 獲取 LD_LIBRARY_PATH 環(huán)境變量值
const char* library_path = getenv("LD_LIBRARY_PATH");
#if defined(HAVE_ANDROID_OS)
if (library_path == NULL) {
//2.如果 1 步驟沒獲取到路徑,那么通過該方法獲取 so 庫的目錄路徑
android_get_LD_LIBRARY_PATH(path, sizeof(path));
library_path = path;
}
#endif
if (library_path == NULL) {
library_path = "";
}
//3.設置 java.library.path 屬性值
properties.push_back(std::string("java.library.path=") + library_path);
return toStringArray(env, properties);
}
沒錯吧漏策,對應的 native 層的方法是上述這個派哲,它干的事,其實也是設置一些屬性值掺喻,我們想要的 java.library.path
就是在這里設置的芭届。那么储矩,這個屬性值來源的邏輯是這樣的:
- 先讀取 LD_LIBRARY_PATH 環(huán)境變量值,如果不為空褂乍,就以這個值為準持隧。但我測試過,貌似逃片,程序運行時讀取的這個值一直是 null屡拨,在 Runtime 的
doLoad()
方法注釋中,Google 有解釋是說由于 Android 的進程都是通過 Zygote 進程 fork 過來题诵,所以不能使用 LD_LIBRARY_PATH 洁仗。應該,大概性锭,可能是這個意思吧赠潦,我英文不大好,你們可以自行去確認一下草冈。 - 也就是說她奥,第一步讀取的 LD_LIBRARY_PATH 值是為空,所以會進入第二步怎棱,調用 android_get_LD_LIBRARY_PATH 方法來讀取屬性值帮坚。(進入這個步驟有個條件是定義了 HAVE_ANDROID_OS 宏變量十减,我就不去找到底哪里在什么場景下會定義了,看命名我直接猜測 Android 系統(tǒng)就都有定義的了)
那么,繼續(xù)看看 android_get_LD_LIBRARY_PATH 這個方法做了些什么:
//platform/libcore/luni/src/main/native/java_lang_System.cpp
#if defined(HAVE_ANDROID_OS)
extern "C" void android_get_LD_LIBRARY_PATH(char*, size_t);
#endif
emmm拳锚,看不懂诈铛,頭疼离福。那仇祭,直接全局搜索下這個方法名試試看吧,結果在另一個 cpp 中找到它的實現(xiàn):
//platform/bionic/linker/dlfcn.cpp
void android_get_LD_LIBRARY_PATH(char* buffer, size_t buffer_size) {
ScopedPthreadMutexLocker locker(&g_dl_mutex);
do_android_get_LD_LIBRARY_PATH(buffer, buffer_size);
}
第一行估計是加鎖之類的意思吧梆暖,不管伞访,第二行是調用另一個方法,繼續(xù)跟下去看看:
//platform/bionic/linker/linker.cpp
void do_android_get_LD_LIBRARY_PATH(char* buffer, size_t buffer_size) {
//...
char* end = stpcpy(buffer, kDefaultLdPaths[0]);
*end = ':';
strcpy(end + 1, kDefaultLdPaths[1]);
}
static const char* const kDefaultLdPaths[] = {
#if defined(__LP64__)
"/vendor/lib64",
"/system/lib64",
#else
"/vendor/lib",
"/system/lib",
#endif
nullptr
};
還好 Source Insight 點擊方法時有時可以支持直接跳轉過去轰驳,調用的這個方法又是在另一個 cpp 文件中了厚掷。開頭省略了一些大小空間校驗的代碼,然后直接復制了靜態(tài)常量的值级解,而這個靜態(tài)常量在這份文件頂部定義冒黑。
終于跟到底了吧,也就是說蠕趁,如果有定義了 __LP64__ 這個宏變量薛闪,那么就將 java.library.path
屬性值賦值為 "/vendor/lib64:/system/lib64",否則俺陋,就賦值為 "/vendor/lib:/system/lib"豁延。
也就是說,so 文件的目錄地址其實是在 native 層通過硬編碼方式寫死的腊状,網(wǎng)上那些理所當然的說 so 文件的存放目錄也就是這四個诱咏,是這么來的。那么缴挖,說白了袋狞,系統(tǒng)默認存放 so 文件的目錄就兩個,只是有兩種場景映屋。
而至于到底什么場景下會有這個 __LP64__ 宏變量的定義苟鸯,什么時候沒有,我實在沒能力繼續(xù)跟蹤下去了棚点,網(wǎng)上搜索了一些資料后早处,仍舊不是很懂,如果有清楚的大佬瘫析,能夠告知砌梆、指點下就最棒了。
我自己看了些資料贬循,以及咸包,自己也做個測試:同一個 app,修改它的 primaryCpuAbi 值杖虾,調用 System 的 getProperty()
來讀取 java.library.path
烂瘫,它返回的值是會不同的。
所以奇适,以我目前的能力以及所掌握的知識滤愕,我是這么猜測的,純屬個人猜測:
__LP64__ 這個宏變量并不是由安卓系統(tǒng)代碼來定義的注竿,而是 Linux 系統(tǒng)層面所定義的魂贬。在 Linux 系統(tǒng)中付燥,可執(zhí)行文件闻丑,也可以說所運行的程序勋锤,如果是 32 位的,那么是沒有定義這個宏變量的,如果是 64 位的,那么是有定義這個宏變量的。
總之滋恬,通俗的聯(lián)想解釋鼓寺,__LP64__ 這個宏變量表示著當前程序是 32 位還是 64 位的意思敢靡。(個人理解)
有時間再繼續(xù)研究吧幔虏,反正這里清楚了陷谱,系統(tǒng)默認存放 so 文件的目錄只有兩個,但有兩種場景夷都。vendor 較少用冬阳,就不每次都打出來了刑顺。也就是說狼讨,如果應用在 system/lib 目錄中沒有找到 so 文件朽基,那么它是不會再自動去 system/lib64 中尋找的,兩者它只會選其一。至于選擇哪個排苍,因為 Zygote 是有分 32 位還是 64 位進程的腻暮,那么剛好可以根據(jù)這個為依據(jù)侈离。
findLibrary
該走回主線了起宽,在支線中的探索已經(jīng)摸索了些經(jīng)驗了。
大伙應該還記得吧叉弦,System 調用了 loadlibrary()
之后颖御,內部其實是調用了 Runtime 的 loadLibrary()
方法,這個方法內部會去調用 ClassLoader 的 findLibrary()
方法,主要是去尋找這個 so 文件是否存在,如果存在,會返回 so 文件的絕對路徑,接著交由 Runtime 的 doLoad()
方法去加載 so 文件糙捺。
所以,我們想要梳理清楚 so 文件的加載流程,findLibrary()
是關鍵。那么岭接,接下去,就來跟著 findLibrary()
走下去看看吧:
//ClassLoader#findLibrary()
protected String findLibrary(String libName) {
return null;
}
ClassLoader 只是一個基類入偷,具體實現(xiàn)在其子類殿雪,那這里具體運行的是哪個子類呢亏镰?
//System#loadlibrary()
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
所以這里是調用了 VMStack 的一個方法來獲取 ClassLoader 對象,那么繼續(xù)跟進看看:
native public static ClassLoader getCallingClassLoader();
又是一個 native 的方法,我嘗試過跟進去,沒有看懂。那么,換個方向來找出這個基類的具體實現(xiàn)子類是哪個吧,很簡單的一個方法,打 log 輸出這個對象本身:
ClassLoader classLoader = getClassLoader();
Log.v(TAG, "classLoader = " + classLoader.toString());
//輸出
// classLoader = dalvik.system.PathClassLoader[dexPath=/data/app/com.qrcode.qrcode-1.apk,libraryPath=/data/app-lib/com.qrcode.qrcode-1]
以上打 Log 代碼是從 Java中System.loadLibrary() 的執(zhí)行過程 這篇文章中截取出來的骆姐,使用這個方法的前提是你得清楚 VMStack 的 getCallingClassLoader()
含義其實是獲取調用這個方法的類它的類加載器對象。
或者归园,你對 Android 的類加載機制有所了解捻浦,知道當啟動某個 app 時钠四,經(jīng)過層層工作后甸祭,會接著讓 LoadedApk 去加載這個 app 的 apk凡怎,然后通過 ApplicationLoader 來加載相關代碼文件,而這個類內部是實例化了一個 PathClassLoader 對象去進行 dex 的加載坛缕。
不管哪種方式骗卜,總之清楚了這里實際上是調用了 PathClassLoader 的 findLibrary()
方法,但 PathClassLoader 內部并沒有這個方法,它繼承自 BaseDexClassLoader,所以實際上還是調用了父類的方法,跟進去看看:
//platform/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
private final DexPathList pathList;
內部又調用了 DexPathList 的 findLibrary()
方法,繼續(xù)跟進看看:
//platform/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public String findLibrary(String libraryName) {
//1. 拼接前綴:lib,和后綴:.so
String fileName = System.mapLibraryName(libraryName);
//2. 遍歷所有存放 so 文件的目錄漆羔,確認指定文件是否存在以及是只讀文件
for (File directory: nativeLibraryDirectories) {
String path = new File(directory, fileName).getPath();
if (IoUtils.canOpenReadOnly(path)) {
return path;
}
}
return null;
}
/** List of native library directories. */
private final File[] nativeLibraryDirectories;
到了這里鸟顺,會先進行文件名補全操作,拼接上前綴:lib 和后綴:.so,然后遍歷所有存放 so 文件的目錄曲楚,當找到指定文件,且是只讀屬性,則返回該 so 文件的絕對路徑惋耙。
所以婿屹,重點就是 nativeLibraryDirectories 這個變量了蜂奸,這里存放著 so 文件存儲的目錄路徑朴乖,那么得看看它在哪里被賦值了:
//platform/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
//...
//1. 唯一賦值的地方畜普,構造函數(shù)
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
private static File[] splitLibraryPath(String path) {
// Native libraries may exist in both the system and
// application library paths, and we use this search order:
//
// 1. this class loader's library path for application libraries
// 2. the VM's library path from the system property for system libraries
// (翻譯下,大體是說儒鹿,so 文件的來源有兩處:1是應用自身存放 so 文件的目錄,2是系統(tǒng)指定的目錄)
// This order was reversed prior to Gingerbread; see http://b/2933456.
ArrayList < File > result = splitPaths(path, System.getProperty("java.library.path"), true);
return result.toArray(new File[result.size()]);
}
//將傳入的兩個參數(shù)的目錄地址解析完都存放到集合中
private static ArrayList < File > splitPaths(String path1, String path2, boolean wantDirectories) {
ArrayList < File > result = new ArrayList < File > ();
splitAndAdd(path1, wantDirectories, result);
splitAndAdd(path2, wantDirectories, result);
return result;
}
private static void splitAndAdd(String searchPath, boolean directoriesOnly, ArrayList < File > resultList) {
if (searchPath == null) {
return;
}
//因為獲取系統(tǒng)的 java.library.path 屬性值返回的路徑是通過 : 拼接的几晤,所以先拆分,然后判斷這些目錄是否可用
for (String path: searchPath.split(":")) {
try {
StructStat sb = Libcore.os.stat(path);
if (!directoriesOnly || S_ISDIR(sb.st_mode)) {
resultList.add(new File(path));
}
} catch(ErrnoException ignored) {}
}
}
所以蟹瘾,nativeLibraryDirectories 這個變量是在構造函數(shù)中被賦值圾浅。代碼不多,總結一下憾朴,構造函數(shù)會傳入一個 libraryPath 參數(shù)狸捕,表示應用自身存放 so 文件的路徑,然后內部會再去調用 System 的 getProperty("java.library.path")
方法獲取系統(tǒng)指定的 so 文件目錄地址众雷。最后灸拍,將這些路徑都添加到集合中做祝。
而且,看添加的順序鸡岗,是先添加應用自身的 so 文件目錄混槐,然后再添加系統(tǒng)指定的 so 文件目錄,也就是說轩性,當加載 so 文件時声登,是先去應用自身的 so 文件目錄地址尋找,沒有找到揣苏,才會去系統(tǒng)指定的目錄悯嗓。
而系統(tǒng)指定的目錄地址在 native 層的 linker.cpp 文件定義,分兩種場景卸察,取決于應用當前的進程是 32 位還是 64 位脯厨,32 位的話,則按順序分別去 vendor/lib 和 system/lib 目錄中尋找蛾派,64 位則是相對應的 lib64 目錄中俄认。
雖然,so 文件加載流程大體清楚了洪乍,但還有兩個疑問點:
- 構造方法參數(shù)傳入的表示應用自身存放 so 文件目錄的 libraryPath 值是哪里來的眯杏;
- 應用什么時候運行在 32 位或 64 位的進程上;
nativeLibraryDir
先看第一個疑問點壳澳,應用自身存放 so 文件目錄的這個值岂贩,要追究的話,這是一個很漫長的故事巷波。
這個過程萎津,我不打算全部都貼代碼了,因為很多步驟抹镊,我自己也沒有去看源碼锉屈,也是看的別人的文章,我們以倒著追蹤的方式來進行追溯吧垮耳。
首先颈渊,這個 libraryPath 值是通過 DexPathList 的構造方法傳入的,而 BaseDexClassLoader 內部的 DexPathList 對象實例化的地方也是在它自己的構造方法中终佛,同樣俊嗽,它也接收一個 libraryPath 參數(shù)值,所以 BaseDexClassLoader 只是做轉發(fā)铃彰,來源并不在它這里绍豁。
那么,再往回走牙捉,就是 LoadedApk 實例化 PathClassLoader 對象的地方了竹揍,在它的 getClassLoader()
方法中:
//platform/frameworks/base/core/java/android/app/LoadedApk.java
public ClassLoader getClassLoader() {
synchronized(this) {
//...
final ArrayList < String > libPaths = new ArrayList < >();
//...
libPaths.add(mLibDir);
//...
final String lib = TextUtils.join(File.pathSeparator, libPaths);
//...
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib, mBaseClassLoader);
//...
}
}
public LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) {
//...
mLibDir = aInfo.nativeLibraryDir;
//...
}
無關代碼都省略掉了敬飒,也就是說,傳給 DexPathList 的 libraryPath 值鬼佣,其實是將要啟動的這個 app 的 ApplicationInfo 中的 nativeLibraryDir 變量值驶拱。
可以看看 ApplicationInfo 中這個變量的注釋:
//ApplicationInfo
/**
* Full path to the directory where native JNI libraries are stored.
* 存放 so 文件的絕對路徑
*/
public String nativeLibraryDir;
通俗點解釋也就是,存放應用自身 so 文件的目錄的絕對路徑晶衷。那么問題又來了蓝纲,傳給 LoadedApk 的這個 ApplicationInfo 對象哪里來的呢?
這個就又涉及到應用的啟動流程了晌纫,大概講一下:
我們知道税迷,當要啟動其他應用時,其實是通過發(fā)送一個 Intent 去啟動這個 app 的 LAUNCHER 標志的 Activity锹漱。而當這個 Intent 發(fā)送出去后箭养,是通過 Binder 通信方式通知了 ActivityManagerServer 去啟動這個 Activity。
AMS 在這個過程中會做很多事哥牍,但在所有事之前毕泌,它得先解析 Intent,知道要啟動的是哪個 app 才能繼續(xù)接下去的工作嗅辣,這個工作在 ActivityStackSupervisor 的 resolveActivity()
:
//ActivityStackSupervisor.java
ActivityInfo resolveActivity(Intent intent, String resolvedType, int startFlags, ProfilerInfo profilerInfo, int userId) {
// Collect information about the target of the Intent.
ActivityInfo aInfo;
try {
ResolveInfo rInfo = AppGlobals.getPackageManager().resolveIntent(intent, resolvedType, PackageManager.MATCH_DEFAULT_ONLY | ActivityManagerService.STOCK_PM_FLAGS, userId);
aInfo = rInfo != null ? rInfo.activityInfo: null;
} catch(RemoteException e) {
aInfo = null;
}
//...
}
不同版本撼泛,可能不是由這個類負責這個工作了,但可以跟著 ActivityManagerService 的 startActivity()
走下去看看澡谭,不用跟很深就能找到愿题,因為這個工作是比較早進行的。
所以蛙奖,解析 Intent 獲取 app 的相關信息就又交給 PackageManagerService 的 resolveIntent()
進行了潘酗,PKMS 的工作不貼了,我直接說了吧:
PKMS 會根據(jù) Intent 中目標組件的 packageName雁仲,通過一個只有包權限的類 Settings 來獲取對應的 ApplicationInfo 信息仔夺,這個 Settings 類全名:com.android.server.pm.Settings,它的職責之一是存儲所有 app 的基本信息攒砖,也就是在 data/system/packages.xml 中各 app 的信息都交由它維護緩存囚灼。
所以,一個 app 的 ApplicationInfo 信息祭衩,包括 nativeLibraryDir 我們都可以在 data/system/packages.xml 這份文件中查看到。這份文件的角色我把它理解成類似 PC 端上的注冊表阅签,所有 app 的信息都注冊在這里掐暮。
那這份 packages.xml 文件的數(shù)據(jù)又是從哪里來的呢,這又得涉及到 apk 的安裝機制過程了政钟。
簡單說一下路克,一個 app 的安裝過程樟结,在解析 apk 包過程中,還會結合各種設備因素等等來決定這個 app 的各種屬性精算,比如說 nativeLibraryDir 這個屬性值的確認瓢宦,就需要考慮這個 app 是三方應用還是系統(tǒng)應用,這個應用的 primaryCpuAbi 屬性值是什么灰羽,apk 文件的地址等等因素后驮履,最后才確定了應用存放 so 文件的目錄地址是哪里。
舉個例子廉嚼,對于系統(tǒng)應用來說玫镐,這個 nativeLibraryDir 值有可能最后是 /system/lib/xxx,也有可能是 system/app/xxx/lib 等等怠噪;而對于三方應用來說恐似,這值有可能就是 data/app/xxx/lib;
也就是說傍念,當 app 安裝完成時矫夷,這些屬性值也就都解析到了,就都會保存到 Settings 中憋槐,同時會將這些信息寫入到 data/system/packages.xml 中双藕。
到這里,先來小結一下秦陋,梳理下前面的內容:
當一個 app 安裝的時候蔓彩,系統(tǒng)會經(jīng)過各種因素考量,最后確認 app 的一個 nativeLibraryDir 屬性值驳概,這個屬性值代表應用自身的 so 文件存放地址赤嚼,這個值也可以在 data/system/packages.xml 中查看。
當應用調用了 System 的 loadlibrary()
時顺又,這個 so 文件的加載流程如下:
- 先到 nativeLibraryDir 指向的目錄地址中尋找這個 so 文件是否存在更卒、可用;
- 如果沒找到稚照,那么根據(jù)應用進程是 32 位或者 64 位來決定下去應該去哪個目錄尋找 so 文件蹂空;
- 如果是 32 位,則先去 vendor/lib 找果录,最后再去 system/lib 中尋找上枕;
- 如果是 64 位,則先去 vendor/lib64 找弱恒,最后再去 system/lib64 中尋找辨萍;
- 系統(tǒng)默認的目錄是在 native 層中的 linker.cpp 文件中指定,更嚴謹?shù)恼f法返弹,不是進程是不是 32 位或 64 位锈玉,而是是否有定義了 __LP64__ 這個宏變量心包。
primaryCpuAbi
我們已經(jīng)清楚了醉蚁,加載 so 文件的流程淆两,其實就分兩步铺敌,先去應用自身存放 so 文件的目錄(nativeLibraryDir)尋找,找不到椅棺,再去系統(tǒng)指定的目錄中尋找犁罩。
而系統(tǒng)指定是目錄分兩種場景,應用進程是 32 位或者 64 位土陪,那么昼汗,怎么知道應用是運行在 32 位還是 64 位的呢?又或者說鬼雀,以什么為依據(jù)來決定一個應用是應該跑在 32 位上還是跑在 64 位上顷窒?
這個就取決于一個重要的屬性了 primaryCpuAbi,它代表著這個應用的 so 文件使用的是哪個 abi 架構源哩。
abi 常見的如:arm64-v8a鞋吉,armeabi-v7a,armeabi励烦,mips谓着,x86_64 等等。
我們在打包 apk 時坛掠,如果不指定赊锚,其實默認是會將所有 abi 對應的 so 文件都打包一份,而通常屉栓,為了減少 apk 包體積舷蒲,我們在 build.gradle 腳本中會指定只打其中一兩份。但不管 apk 包有多少種不同的 abi 的 so 文件友多,在 app 安裝過程中牲平,最終拷貝到 nativeLibraryDir 中的通常都只有一份,除非你手動指定了要多份域滥。
那么纵柿,app 在安裝過程中,怎么知道启绰,應該拷貝 apk 中的 lib 下的哪一份 so 文件呢昂儒?這就是由應用的 primaryCpuAbi 屬性決定。
而同樣委可,這個屬性一樣是在 app 安裝過程中確定的荆忍,這個過程更加復雜,末尾有給了篇鏈接,感興趣可以去看看刹枉,大概來說,就是 apk 包中的 so 文件屈呕、系統(tǒng)應用微宝、相同 UID 的應用、設備的 abilist 等都對這個屬性值的確定過程有所影響虎眨。同樣蟋软,這個屬性值也可以在 data/system/packages.xml 中查看。
那么嗽桩,這個 primaryCpuAbi 屬性值是如何影響應用進程是 32 位還是 64 位的呢岳守?
這就涉及到 Zygote 方面的知識了。
在系統(tǒng)啟動之后碌冶,系統(tǒng)會根據(jù)設備的 ro.zygote 屬性值決定啟動哪個 Zygote湿痢,可以通過執(zhí)行 getprop | grep ro.zygote
來查看這個屬性值,屬性值與對應的 Zygote 進程關系如下:
- zygote32:只啟動一個 32 位的 Zygote 進程
- zygote32_64:啟動兩個 Zygote 進程扑庞,分別為 32 位和 64 位譬重,32 位的進程名為 zygote,表示以它為主罐氨,64 位進程名為 zygote_secondary 臀规,表示它作為輔助
- zygote64:只啟動一個 64 位的 Zygote 進程
- zygote64_32:啟動兩個 Zygote 進程,分別為 32 位和 64 位栅隐,64 位的進程名為 zygote塔嬉,表示以它為主,32 位進程名為 zygote_secondary 租悄,表示它作為輔助
而 Zygote 進程啟動之后谨究,會打開一個 socket 端口,等待 AMS 發(fā)消息過來啟動新的應用時 fork 當前 Zygote 進程恰矩,所以记盒,如果 AMS 是發(fā)給 64 位的 Zygote,那么新的應用自然就是跑在 64 位的進程上外傅;同理纪吮,如果發(fā)給了 32 位的 Zygote 進程,那么 fork 出來的進程自然也就是 32 位的萎胰。
那么碾盟,可以跟隨著 AMS 的 startProcessLocked()
方法,去看看是以什么為依據(jù)選擇 32 位或 64 位的 Zygote:
//ActivityManagerService
private final void startProcessLocked(ProcessRecord app, String hostingType, String hostingNameStr, String abiOverride, String entryPoint, String[] entryPointArgs) {
//...省略
//1. 獲取要啟動的 app 的 primaryCpuAbi 屬性值技竟,abiOverride 不知道是什么冰肴,可能是 Google 開發(fā)人員寫測試用例用的吧,或者其他一些場景
String requiredAbi = (abiOverride != null) ? abiOverride: app.info.primaryCpuAbi;
if (requiredAbi == null) {
//2. 如果為空,以設備支持的首個 abi 屬性值熙尉,可執(zhí)行 getprot ro.product.cpu.abilist 查看
requiredAbi = Build.SUPPORTED_ABIS[0];
}
//...
//3. 調用Precess 的 start 方法联逻,將 requiredAbi 傳入
Process.ProcessStartResult startResult = Process.start(entryPoint, app.processName, uid, uid, gids, debugFlags, mountExternal, app.info.targetSdkVersion, app.info.seinfo, requiredAbi, instructionSet, app.info.dataDir, entryPointArgs);
//...
}
AMS 會先獲取要啟動的 app 的 primaryCpuAbi 屬性值,至于這個 app 的相關信息怎么來的检痰,跟上一小節(jié)一樣包归,解析 Intent 時交由 PKMS 去它模塊內部的 Settings 讀取的。
如果 primaryCpuAbi 為空铅歼,則以設備支持的首個 abi 屬性值為主公壤,設備支持的 abi 列表可以通過執(zhí)行 getprot ro.product.cpu.abilist
查看,最后調用 Precess 的 start()
方法椎椰,將讀取的 abi 值傳入:
//Process
public static final ProcessStartResult start(final String processClass, final String niceName, int uid, int gid, int[] gids, int debugFlags, int mountExternal, int targetSdkVersion, String seInfo, String abi, String instructionSet, String appDataDir, String[] zygoteArgs) {
//...
return startViaZygote(processClass, niceName, uid, gid, gids, debugFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, zygoteArgs);
//...
}
private static ProcessStartResult startViaZygote(final String processClass, final String niceName, final int uid, final int gid, final int[] gids, int debugFlags, int mountExternal, int targetSdkVersion, String seInfo, String abi, String instructionSet, String appDataDir, String[] extraArgs) throws ZygoteStartFailedEx {
//...
//所以 abi 最終是調用 openZygoteSocketIfNeeded() 方法厦幅,傳入給它使用
return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);
}
abi 值又是一層傳一層,最終交到了 Process 的 openZygoteSocketIfNeeded()
方法中使用慨飘,跟進看看:
//Process
private static ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx {
if (primaryZygoteState == null || primaryZygoteState.isClosed()) {
try {
//ZYGOTE_SOCKET值為 zygote,
//通過 ZygoteState 的 connect 方法确憨,連接進程名為 zygote 的 Zygote 進程
primaryZygoteState = ZygoteState.connect(ZYGOTE_SOCKET);
} catch(IOException ioe) {
throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe);
}
}
//在進程名為 zygote 的 Zygote 進程支持的 abi 列表中,查看是否支持要啟動的 app 的需要的 abi
if (primaryZygoteState.matches(abi)) {
return primaryZygoteState;
}
// The primary zygote didn't match. Try the secondary.
if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) {
try {
//SECONDARY_ZYGOTE_SOCKET 的值為 zygote_secondary,
//通過 ZygoteState 的 connect 方法套媚,連接進程名為 zygote_secondary 的 Zygote 進程
secondaryZygoteState = ZygoteState.connect(SECONDARY_ZYGOTE_SOCKET);
} catch(IOException ioe) {
throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe);
}
}
//在進程名為 zygote_secondary 的 Zygote 進程支持的 abi 列表中缚态,查看是否支持要啟動的 app 的需要的 abi
if (secondaryZygoteState.matches(abi)) {
return secondaryZygoteState;
}
throw new ZygoteStartFailedEx("Unsupported zygote ABI: " + abi);
}
static ZygoteState primaryZygoteState;
static ZygoteState secondaryZygoteState;
public static final String ZYGOTE_SOCKET = "zygote";
public static final String SECONDARY_ZYGOTE_SOCKET = "zygote_secondary";
到了這里,是先獲取進程名 zygote 的 Zygote 進程堤瘤,查看它支持的 abi 列表中是否滿足要啟動的 app 所需的 abi玫芦,如果滿足,則使用這個 Zygote 來 fork 新進程本辐,否則桥帆,獲取另一個進程名為 zygote_secondary 的 Zygote 進程,同樣查看它支持的 abi 列表中是否滿足 app 所需的 abi慎皱,如果都不滿足老虫,拋異常。
那么茫多,名為 zygote 和 zygote_secondary 分別對應的是哪個 Zygote 進程呢祈匙?哪個對應 32 位,哪個對應 64 位天揖?
還記得上述說過的夺欲,系統(tǒng)啟動后,會去根據(jù)設備的 ro.zygote 屬性決定啟動哪個 Zygote 進程嗎今膊?對應關系就是這個屬性值決定的些阅,舉個例子,可以看看 zygote64_32 對應的 Zygote 啟動配置文件:
//platform/system/core/rootdir/init.zygote64_32.rc
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
class main
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
service zygote_secondary /system/bin/app_process32 -Xzygote /system/bin --zygote --socket-name=zygote_secondary
class main
socket zygote_secondary stream 660 root system
onrestart restart zygote
這份代碼前半段的意思就表示斑唬,讓 Linux 啟動一個 service市埋,進程名為 zygote黎泣,可執(zhí)行文件位于 /system/bin/app_process64,后面是參數(shù)以及其他命令缤谎。
所以抒倚,名為 zygote 和 zygote_secondary 分別對應的是哪個 Zygote 進程,就取決于設備的 ro.zygote 屬性坷澡。
而衡便,獲取 Zygote 支持的 abi 列表是通過 ZygoteState 的 connect()
方法,我們繼續(xù)跟進看看:
//Process$ZygoteState
public static ZygoteState connect(String socketAddress) throws IOException {
//...
String abiListString = getAbiList(zygoteWriter, zygoteInputStream);
Log.i("Zygote", "Process: zygote socket opened, supported ABIS: " + abiListString);
return new ZygoteState(zygoteSocket, zygoteInputStream, zygoteWriter, Arrays.asList(abiListString.split(",")));
}
發(fā)現(xiàn)沒有洋访,源碼內部將 Zygote 支持的 abi 列表輸出日志了,你們可以自己嘗試下谴餐,過濾下 TAG 為 Zygote姻政,然后重啟下設備,因為如果本來就連著 Zygote岂嗓,那么是不會走到這里的了汁展,最后看一下相關日志,如:
01-01 08:00:13.509 2818-2818/? D/AndroidRuntime: >>>>>> START com.android.internal.os.ZygoteInit uid 0 <<<<<<
01-01 08:00:15.068 2818-2818/? D/Zygote: begin preload
01-01 08:00:15.081 2818-3096/? I/Zygote: Preloading classes...
01-01 08:00:15.409 2818-3097/? I/Zygote: Preloading resources...
01-01 08:00:16.637 2818-3097/? I/Zygote: ...preloaded 343 resources in 1228ms.
01-01 08:00:16.669 2818-3097/? I/Zygote: ...preloaded 41 resources in 33ms.
01-01 08:00:17.242 2818-3096/? I/Zygote: ...preloaded 3005 classes in 2161ms.
01-01 08:00:17.373 2818-2818/? I/Zygote: Preloading shared libraries...
01-01 08:00:17.389 2818-2818/? D/Zygote: end preload
01-01 08:00:17.492 2818-2818/? I/Zygote: System server process 3102 has been created
01-01 08:00:17.495 2818-2818/? I/Zygote: Accepting command socket connections
01-01 08:00:32.789 3102-3121/? I/Zygote: Process: zygote socket opened, supported ABIS: armeabi-v7a,armeabi
系統(tǒng)啟動后厌殉,Zygote 工作的相關內容基本都打日志出來了食绿。
最后,再來稍微理一理:
app 安裝過程公罕,會確定 app 的一個屬性值:primaryCpuAbi器紧,它代表著這個應用的 so 文件使用的是哪個 abi 架構,而且它的確定過程很復雜楼眷,apk 包中的 so 文件铲汪、系統(tǒng)應用、相同 UID 的應用罐柳、設備的 abilist 等都對這個屬性值的確定過程有所影響掌腰。安裝成功后,可以在 data/system/packages.xml 中查看這個屬性值张吉。
每啟動一個新的應用齿梁,都是運行在新的進程中,而新的進程是從 Zygote 進程 fork 過來的肮蛹,系統(tǒng)在啟動時勺择,會根據(jù)設備的 ro.zygote 屬性值決定啟動哪幾個 Zygote 進程,然后打開 socket蔗崎,等待 AMS 發(fā)送消息來 fork 新進程酵幕。
當系統(tǒng)要啟動一個新的應用時,AMS 在負責這個工作進行到 Process 類的工作時缓苛,會先嘗試在進程名為 zygote 的 Zygote 進程中芳撒,查看它所支持的 abi 列表中是否滿足要啟動的 app 所需的 abi邓深,如果滿足,則以這個 Zygote 為主笔刹,fork 新進程芥备,運行在 32 位還是 64 位就跟這個 Zygote 進程一致,而 Zygote 運行在幾位上取決于 ro.zygote 對應的文件舌菜,如值為 zygote64_32 時萌壳,對應著 init.zygote64_32.rc 這份文件,那么此時名為 zygote 的 Zygote 就是運行在 64 位上的日月。
而當上述所找的 Zygote 支持的 abi 列表不滿足 app 所需的 abi 時袱瓮,那么再去名為 zygote_secondary 的 Zygote 進程中看看,它所支持的 abi 列表是否滿足爱咬。
另外尺借,Zygote 的相關工作流程,包括支持的 abi 列表精拟,系統(tǒng)都有打印相關日志燎斩,可過濾 Zygote 查看,如沒發(fā)現(xiàn)蜂绎,可重啟設備查看栅表。
abi 兼容
so 文件加載的流程,及應用運行在 32 位或 64 位的依據(jù)我們都梳理完了师枣,以上內容足夠掌握什么場景下怪瓶,該去哪些目錄下加載 so 文件的判斷能力了。
那么坛吁,還有個問題劳殖,如果應用運行在 64 位上,那么此時拨脉,它是否能夠使用 armeabi-v7a 的 so 文件哆姻?
首先,先來羅列一下常見的 abi :
- arm64-v8a玫膀,armeabi-v7a矛缨,armeabi,mips帖旨,mips64箕昭,x86,x86_64
其中解阅,運行在 64 位的 Zygote 進程上的是:
- arm64-v8a落竹,mips64,x86_64
同樣货抄,運行在 32 位的 Zygote 進程上的是:
- armeabi-v7a述召,armeabi朱转,mips,x86
你們如果去網(wǎng)上搜索如下關鍵字:so 文件积暖,abi 兼容等藤为,你們會發(fā)現(xiàn),蠻多文章里都會說:arm64-v8a 的設備能夠向下兼容夺刑,支持運行 32 位的 so 文件缅疟,如 armeabi-v7a。
這句話沒錯遍愿,64 位的設備能夠兼容運行 32 位的 so 文件存淫,但別只看到這句話啊,良心一些的文章里還有另一句話:不同 cpu 架構的 so 文件不能夠混合使用沼填,例如纫雁,程序運行期間,要么全部使用 arm64-v8a 的 so 文件倾哺,要么全部使用 armeabi-v7a 的 so 文件,你不能跑在 64 位進程上刽脖,卻使用著 32 位的 so 文件羞海。
我所理解的兼容,并不是說曲管,64 位的設備却邓,支持你運行在 64 位的 Zygote 進程上時仍舊可以使用 32 位的 so 文件。有些文章里也說了院水,如果在 64 位的設備上腊徙,你選擇使用 32 位的 so 文件,那么此時檬某,你就丟失了專門為 64 位優(yōu)化過的性能(ART撬腾,webview,media等等 )恢恼。這個意思就是說民傻,程序啟動時是從 32 位的 Zygote 進程 fork 過來的,等于你在 64 位的設備上场斑,但卻只運行在 32 位的進程上漓踢。
至于程序如何決定運行在 32 位還是 64 位,上面的章節(jié)中也分析過了漏隐,以 app 的 primaryCpuAbi 屬性值為主喧半,而這個屬性值的確定因素之一就是含有的 so 文件所屬的 abi。
如果青责,你還想自己驗證挺据,那么可以跟著 Runtime 的 doLoad()
方法跟到 native 層去看看取具,由于我下載的源碼版本可能有些問題,我沒找到 Runtime 對應的 cpp 文件吴菠,但我找到這么段代碼:
//platform/bionic/linker/linker_phdr.cpp
bool ElfReader::VerifyElfHeader() {
//...
//1.讀取 elf 文件的 header 的 class 信息
int elf_class = header_.e_ident[EI_CLASS];
#if defined(__LP64__)
//2. 如果當前進程是64位的者填,而 elf 文件屬于 32 位的,則報錯
if (elf_class != ELFCLASS64) {
if (elf_class == ELFCLASS32) {
DL_ERR("\"%s\" is 32-bit instead of 64-bit", name_);
} else {
DL_ERR("\"%s\" has unknown ELF class: %d", name_, elf_class);
}
return false;
}
#else
//3. 如果當前進程是32位的做葵,而 elf 文件屬于 64 位的占哟,則報錯
if (elf_class != ELFCLASS32) {
if (elf_class == ELFCLASS64) {
DL_ERR("\"%s\" is 64-bit instead of 32-bit", name_);
} else {
DL_ERR("\"%s\" has unknown ELF class: %d", name_, elf_class);
}
return false;
}
#endif
加載 so 文件,最終還是交由 native 層去加載酿矢,在 Linux 中榨乎,so 文件其實就是一個 elf 文件,elf 文件有個 header 頭部信息瘫筐,里面記錄著這份文件的一些信息蜜暑,如所屬的是 32 位還是 64 位,abi 的信息等等策肝。
而 native 層在加載 so 文件之前肛捍,會去解析這個 header 信息,當發(fā)現(xiàn)之众,如果當前進程運行在 64 位時拙毫,但要加載的 so 文件卻是 32 位的,就會報 xxx is 32-bit instead of 64-bit
異常棺禾,同樣缀蹄,如果當前進程是運行在 32 位的,但 so 文件卻是 64 位的膘婶,此時報 xxx is 64-bit instead of 32-bit
異常缺前。
這個異常應該也有碰見過吧:
java.lang.UnsatisfiedLinkError: dlopen failed: "libimagepipeline.so" is 32-bit instead of 64-bit
所以說,64 位設備的兼容悬襟,并不是說衅码,允許你運行在 64 位的進程上時,仍舊可以使用 32 位的 so 文件脊岳。它的兼容是說肆良,允許你在 64 位的設備上運行 32 位的進程。
其實逸绎,想想也能明白惹恃,這就是為什么三方應用安裝的時候,并不會將 apk 包中所有 abi 目錄下的 so 文件都解壓出來棺牧,只會解壓一種巫糙,因為應用在安裝過程中,系統(tǒng)已經(jīng)確定你這個應用是應該運行在 64 位還是 32 位的進程上了颊乘,并將這個結果保存在 app 的 primaryCpuAbi 屬性值中参淹。
既然系統(tǒng)已經(jīng)明確你的應用所運行的進程是 32 位還是 64 位醉锄,那么只需拷貝對應的一份 so 文件即可,畢竟 64 位的 so 文件和 32 位的又不能混合使用浙值。
以上恳不,是我的理解,如果有誤开呐,歡迎指點下烟勋。
總結
整篇梳理下來,雖然梳理 so 的加載流程不難筐付,但要掌握知其所以然的程度卵惦,就需要多花費一點心思了。
畢竟都涉及到應用的安裝機制瓦戚,應用啟動流程沮尿,系統(tǒng)啟動機制兽泣,Zygote 相關的知識點了亡笑。如果你是開發(fā)系統(tǒng)應用的逼友,建議還是花時間整篇看一下子漩,畢竟系統(tǒng)應用的集成不像三方應用那樣在 apk 安裝期間自動將相關 so 文件解壓到 nativeLibraryDirectories 路徑下了。三方應用很少需要了解 so 的加載流程咬腋,但開發(fā)系統(tǒng)應用還是清楚點比較好吸申。
不管怎么說两嘴,有時間当编,可以稍微跟著過一下整篇,相信多少是會有些收獲的徒溪,如果發(fā)現(xiàn)哪里有誤忿偷,也歡迎指點。沒時間的話臊泌,那就看看總結吧鲤桥。
- 一個應用在安裝過程中,系統(tǒng)會經(jīng)過一系列復雜的邏輯確定兩個跟 so 文件加載相關的 app 屬性值:nativeLibraryDirectories 渠概,primaryCpuAbi 茶凳;
- nativeLibraryDirectories 表示應用自身存放 so 文件的目錄地址,影響著 so 文件的加載流程播揪;
- primaryCpuAbi 表示應用應該運行在哪種 abi 上贮喧,如(armeabi-v7a),它影響著應用是運行在 32 位還是 64 位的進程上猪狈,進而影響到尋找系統(tǒng)指定的 so 文件目錄的流程箱沦;
- 以上兩個屬性,在應用安裝結束后雇庙,可在 data/system/packages.xml 中查看谓形;
- 當調用 System 的
loadLibrary()
加載 so 文件時灶伊,流程如下: - 先到 nativeLibraryDirectories 指向的目錄中尋找,是否存在且可用的 so 文件寒跳,有則直接加載這里的 so 文件聘萨;
- 上一步?jīng)]找到的話,則根據(jù)當前進程如果是 32 位的童太,那么依次去 vendor/lib 和 system/lib 目錄中尋找米辐;
- 同樣,如果當前進程是 64 位的康愤,那么依次去 vendor/lib64 和 system/lib64 目錄中尋找儡循;
- 當前應用是運行在 32 位還是 64 位的進程上,取決于系統(tǒng)的 ro.zygote 屬性和應用的 primaryCpuAbi 屬性值征冷,系統(tǒng)的 ro.zygote 可通過執(zhí)行 getprop 命令查看择膝;
- 如果 ro.zygote 屬性為 zygote64_32,那么應用啟動時检激,會先在 ro.product.cpu.abilist64 列表中尋找是否支持 primaryCpuAbi 屬性肴捉,有,則該應用運行在 64 位的進程上叔收;
- 如果上一步不支持齿穗,那么會在 ro.product.cpu.abilist32 列表中尋找是否支持 primaryCpuAbi 屬性,有饺律,則該應用運行在 32 位的進程上窃页;
- 如果 ro.zygote 屬性為 zygote32_64,則上述兩個步驟互換复濒;
- 如果應用的 primaryCpuAbi 屬性為空脖卖,那么以 ro.product.cpu.abilist 列表中第一個 abi 值作為應用的 primaryCpuAbi;
- 運行在 64 位的 abi 有:arm64-v8a巧颈,mips64畦木,x86_64
- 運行在 32 位的 abi 有:armeabi-v7a,armeabi砸泛,mips十籍,x86
- 通常支持 arm64-v8a 的 64 位設備,都會向下兼容支持 32 位的 abi 運行唇礁;
- 但應用運行期間勾栗,不能混合著使用不同 abi 的 so 文件;
- 比如盏筐,當應用運行在 64 位進程中時械姻,無法使用 32 位 abi 的 so 文件,同樣,應用運行在 32 位進程中時楷拳,也無法使用 64 位 abi 的 so 文件绣夺;
參考資料
1.Android -- 系統(tǒng)進程Zygote的啟動分析
大家好,我是 dasu欢揖,歡迎關注我的公眾號(dasuAndroidTv)陶耍,如果你覺得本篇內容有幫助到你,可以轉載但記得要關注她混,要標明原文哦烈钞,謝謝支持~