Android 的 so 文件加載機制


本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發布

最近碰到一些 so 文件問題,順便將相關知識點梳理一下。

提問

本文的結論是跟着 System.loadlibrary() 一層層源碼走進去,個人對其的理解所整理的,那么開始看源碼之前,先來提幾個問題:

Q1:你知道 so 文件的加載流程嗎?

Q2:設備存放 so 的路徑有 system/lib,vendor/lib,system/lib64,vendor/lib64,知道在哪里規定了這些路徑嗎?清楚哪些場景下系統會去哪個目錄下尋找 so 文件嗎?還是說,所有的目錄都會去尋找?

Q3:Zygote 進程是分 32 位和 64 位的,那么,系統是如何決定某個應用應該運行在 32 位上,還是 64 位上?

Q4:如果程序跑在 64 位的 Zygote 進程上時,可以使用 32 位的 so 文件么,即應用的 primaryCpuAbi 為 arm64-v8a,那么是否可使用 armeabi-v7a 的 so 文件,兼容的嗎?

Q2,Q3,Q4,這幾個問題都是基於設備支持 64 位的前提下,在舊系統版本中,只支持 32 位,也就沒這么多疑問需要處理了。

源碼

准備工作

由於這次的源碼會涉及很多 framework 層的代碼,包括 java 和 c++,直接在 AndroidStudio 跟進 SDK 的源碼已不足夠查看到相關的代碼了。所以,此次是借助 Source Insight 軟件,而源碼來源如下:

https://android.googlesource.com/platform/

我並沒有將所有目錄下載下來,只下載了如下目錄的源碼:

我沒有下載最新版本的代碼,而是選擇了 Tags 下的 More 按鈕,然后選擇 tag 為: android-5.1.1 r24 的代碼下載。所以,此次分析的源碼是基於這個版本,其余不同版本的代碼可能會有所不一樣,但大體流程應該都是一致的。

分析

源碼分析的過程很長很長,不想看過程的話,你也可以直接跳到末尾看結論,但就會錯失很多細節的分析了。

那么下面就開始來過下源碼吧,分析的入口就是跟着 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(),不同系統版本,這些代碼是有些許差別的,但不管怎樣,重點都還是 loadLibrary() 中調用的一些方法,這些方法基本沒變,改變的只是其他代碼的優化寫法。

那么,要理清 so 文件的加載流程,或者說,要找出系統是去哪些地址加載 so 文件的,就需要梳理清這些方法:

  • loader.findLibrary()
  • doLoad()

第一個方法用於尋找 so 文件,所涉及的整個流程應該都在這個方法里,如果可以找到,會返回 so 文件的絕對路徑,然后交由 doLoad() 去加載。

java.library.path

但在深入去探索之前,我想先探索另一條分支,loader 為空的場景。loader 什么時候為空,什么時候不為空,我並不清楚,只是看別人的文章分析時說,程序中通過 System.loadlibrary() 方式加載 so,那么 loader 就不會為空。那,我就信你了,不然我也不知道去哪分析為不為空的場景。

既然程序不會走另一個分支,為什么我還要先來探索它呢?因為,第一個分支太不好探索了,先從另一個分支摸索點經驗,而且還發現了一些感覺可以拿來講講的方法:

  • 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 的系統屬性值,是可以獲取到設備存放 so 庫的目錄地址的,那么就來看看在哪里有設置這個屬性值進去。

System 內部有一個類型為 Properties 的靜態變量,不同版本,這個變量名可能不一樣,但作用也都一樣,用來存儲這些系統屬性值,這樣程序需要的時候,調用 getProperty() 讀取屬性值時其實是來這個靜態變量中讀取。而變量的初始化地方在類中的 static 代碼塊中:

//System
static {
    //...省略
    //1.初始化一些不變的系統屬性值
    unchangeableSystemProperties = initUnchangeableSystemProperties();
    //2.將上述的屬性值以及一些默認的系統屬性值設置到靜態變量中
    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.拷貝不可變的一些系統屬性值
    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 靜態代碼塊中的代碼其實就是在初始化系統屬性值,分兩個步驟,一個是先設置一些不可變的屬性值,二是設置一些默認的屬性值,然后將這些存儲在靜態變量中。

但其實,不管在哪個方法中,都沒找到有設置 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 形式的字符串數組,然后 parsePropertyAssignments() 方法會去遍歷這些數組,將這些屬性值填充到存儲系統屬性值的靜態變量中。

也就是說,在 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 環境變量值
    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 就是在這里設置的。那么,這個屬性值來源的邏輯是這樣的:

  1. 先讀取 LD_LIBRARY_PATH 環境變量值,如果不為空,就以這個值為准。但我測試過,貌似,程序運行時讀取的這個值一直是 null,在 Runtime 的 doLoad() 方法注釋中,Google 有解釋是說由於 Android 的進程都是通過 Zygote 進程 fork 過來,所以不能使用 LD_LIBRARY_PATH 。應該,大概,可能是這個意思吧,我英文不大好,你們可以自行去確認一下。
  2. 也就是說,第一步讀取的 LD_LIBRARY_PATH 值是為空,所以會進入第二步,調用 android_get_LD_LIBRARY_PATH 方法來讀取屬性值。(進入這個步驟有個條件是定義了 HAVE_ANDROID_OS 宏變量,我就不去找到底哪里在什么場景下會定義了,看命名我直接猜測 Android 系統就都有定義的了)

那么,繼續看看 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 中找到它的實現:

//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);
}

第一行估計是加鎖之類的意思吧,不管,第二行是調用另一個方法,繼續跟下去看看:

//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 文件中了。開頭省略了一些大小空間校驗的代碼,然后直接復制了靜態常量的值,而這個靜態常量在這份文件頂部定義。

終於跟到底了吧,也就是說,如果有定義了 __LP64__ 這個宏變量,那么就將 java.library.path 屬性值賦值為 "/vendor/lib64:/system/lib64",否則,就賦值為 "/vendor/lib:/system/lib"。

也就是說,so 文件的目錄地址其實是在 native 層通過硬編碼方式寫死的,網上那些理所當然的說 so 文件的存放目錄也就是這四個,是這么來的。那么,說白了,系統默認存放 so 文件的目錄就兩個,只是有兩種場景。

而至於到底什么場景下會有這個 __LP64__ 宏變量的定義,什么時候沒有,我實在沒能力繼續跟蹤下去了,網上搜索了一些資料后,仍舊不是很懂,如果有清楚的大佬,能夠告知、指點下就最棒了。

我自己看了些資料,以及,自己也做個測試:同一個 app,修改它的 primaryCpuAbi 值,調用 System 的 getProperty() 來讀取 java.library.path,它返回的值是會不同的。

所以,以我目前的能力以及所掌握的知識,我是這么猜測的,純屬個人猜測:

__LP64__ 這個宏變量並不是由安卓系統代碼來定義的,而是 Linux 系統層面所定義的。在 Linux 系統中,可執行文件,也可以說所運行的程序,如果是 32 位的,那么是沒有定義這個宏變量的,如果是 64 位的,那么是有定義這個宏變量的。

總之,通俗的聯想解釋,__LP64__ 這個宏變量表示着當前程序是 32 位還是 64 位的意思。(個人理解)

有時間再繼續研究吧,反正這里清楚了,系統默認存放 so 文件的目錄只有兩個,但有兩種場景。vendor 較少用,就不每次都打出來了。也就是說,如果應用在 system/lib 目錄中沒有找到 so 文件,那么它是不會再自動去 system/lib64 中尋找的,兩者它只會選其一。至於選擇哪個,因為 Zygote 是有分 32 位還是 64 位進程的,那么剛好可以根據這個為依據。

findLibrary

該走回主線了,在支線中的探索已經摸索了些經驗了。

大伙應該還記得吧,System 調用了 loadlibrary() 之后,內部其實是調用了 Runtime 的 loadLibrary() 方法,這個方法內部會去調用 ClassLoader 的 findLibrary() 方法,主要是去尋找這個 so 文件是否存在,如果存在,會返回 so 文件的絕對路徑,接着交由 Runtime 的 doLoad() 方法去加載 so 文件。

所以,我們想要梳理清楚 so 文件的加載流程,findLibrary() 是關鍵。那么,接下去,就來跟着 findLibrary() 走下去看看吧:

//ClassLoader#findLibrary()
protected String findLibrary(String libName) {
    return null;
}

ClassLoader 只是一個基類,具體實現在其子類,那這里具體運行的是哪個子類呢?

//System#loadlibrary()
public static void loadLibrary(String libName) {
    Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}

所以這里是調用了 VMStack 的一個方法來獲取 ClassLoader 對象,那么繼續跟進看看:

native public static ClassLoader getCallingClassLoader();

又是一個 native 的方法,我嘗試過跟進去,沒有看懂。那么,換個方向來找出這個基類的具體實現子類是哪個吧,很簡單的一個方法,打 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() 的執行過程 這篇文章中截取出來的,使用這個方法的前提是你得清楚 VMStack 的 getCallingClassLoader() 含義其實是獲取調用這個方法的類它的類加載器對象。

或者,你對 Android 的類加載機制有所了解,知道當啟動某個 app 時,經過層層工作后,會接着讓 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() 方法,繼續跟進看看:

//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. 唯一賦值的地方,構造函數
    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是系統指定的目錄)
    // 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()]);
}

//將傳入的兩個參數的目錄地址解析完都存放到集合中
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;
    }
    //因為獲取系統的 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 這個變量是在構造函數中被賦值。代碼不多,總結一下,構造函數會傳入一個 libraryPath 參數,表示應用自身存放 so 文件的路徑,然后內部會再去調用 System 的 getProperty("java.library.path") 方法獲取系統指定的 so 文件目錄地址。最后,將這些路徑都添加到集合中。

而且,看添加的順序,是先添加應用自身的 so 文件目錄,然后再添加系統指定的 so 文件目錄,也就是說,當加載 so 文件時,是先去應用自身的 so 文件目錄地址尋找,沒有找到,才會去系統指定的目錄。

而系統指定的目錄地址在 native 層的 linker.cpp 文件定義,分兩種場景,取決於應用當前的進程是 32 位還是 64 位,32 位的話,則按順序分別去 vendor/lib 和 system/lib 目錄中尋找,64 位則是相對應的 lib64 目錄中。

雖然,so 文件加載流程大體清楚了,但還有兩個疑問點:

  • 構造方法參數傳入的表示應用自身存放 so 文件目錄的 libraryPath 值是哪里來的;
  • 應用什么時候運行在 32 位或 64 位的進程上;

nativeLibraryDir

先看第一個疑問點,應用自身存放 so 文件目錄的這個值,要追究的話,這是一個很漫長的故事。

這個過程,我不打算全部都貼代碼了,因為很多步驟,我自己也沒有去看源碼,也是看的別人的文章,我們以倒着追蹤的方式來進行追溯吧。

首先,這個 libraryPath 值是通過 DexPathList 的構造方法傳入的,而 BaseDexClassLoader 內部的 DexPathList 對象實例化的地方也是在它自己的構造方法中,同樣,它也接收一個 libraryPath 參數值,所以 BaseDexClassLoader 只是做轉發,來源並不在它這里。

那么,再往回走,就是 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 對象哪里來的呢?

這個就又涉及到應用的啟動流程了,大概講一下:

我們知道,當要啟動其他應用時,其實是通過發送一個 Intent 去啟動這個 app 的 LAUNCHER 標志的 Activity。而當這個 Intent 發送出去后,是通過 Binder 通信方式通知了 ActivityManagerServer 去啟動這個 Activity。

AMS 在這個過程中會做很多事,但在所有事之前,它得先解析 Intent,知道要啟動的是哪個 app 才能繼續接下去的工作,這個工作在 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 會根據 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 文件的數據又是從哪里來的呢,這又得涉及到 apk 的安裝機制過程了。

簡單說一下,一個 app 的安裝過程,在解析 apk 包過程中,還會結合各種設備因素等等來決定這個 app 的各種屬性,比如說 nativeLibraryDir 這個屬性值的確認,就需要考慮這個 app 是三方應用還是系統應用,這個應用的 primaryCpuAbi 屬性值是什么,apk 文件的地址等等因素后,最后才確定了應用存放 so 文件的目錄地址是哪里。

舉個例子,對於系統應用來說,這個 nativeLibraryDir 值有可能最后是 /system/lib/xxx,也有可能是 system/app/xxx/lib 等等;而對於三方應用來說,這值有可能就是 data/app/xxx/lib;

也就是說,當 app 安裝完成時,這些屬性值也就都解析到了,就都會保存到 Settings 中,同時會將這些信息寫入到 data/system/packages.xml 中。

到這里,先來小結一下,梳理下前面的內容:

當一個 app 安裝的時候,系統會經過各種因素考量,最后確認 app 的一個 nativeLibraryDir 屬性值,這個屬性值代表應用自身的 so 文件存放地址,這個值也可以在 data/system/packages.xml 中查看。

當應用調用了 System 的 loadlibrary() 時,這個 so 文件的加載流程如下:

  1. 先到 nativeLibraryDir 指向的目錄地址中尋找這個 so 文件是否存在、可用;
  2. 如果沒找到,那么根據應用進程是 32 位或者 64 位來決定下去應該去哪個目錄尋找 so 文件;
  3. 如果是 32 位,則先去 vendor/lib 找,最后再去 system/lib 中尋找;
  4. 如果是 64 位,則先去 vendor/lib64 找,最后再去 system/lib64 中尋找;
  5. 系統默認的目錄是在 native 層中的 linker.cpp 文件中指定,更嚴謹的說法,不是進程是不是 32 位或 64 位,而是是否有定義了 __LP64__ 這個宏變量。

primaryCpuAbi

我們已經清楚了,加載 so 文件的流程,其實就分兩步,先去應用自身存放 so 文件的目錄(nativeLibraryDir)尋找,找不到,再去系統指定的目錄中尋找。

而系統指定是目錄分兩種場景,應用進程是 32 位或者 64 位,那么,怎么知道應用是運行在 32 位還是 64 位的呢?又或者說,以什么為依據來決定一個應用是應該跑在 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 文件、系統應用、相同 UID 的應用、設備的 abilist 等都對這個屬性值的確定過程有所影響。同樣,這個屬性值也可以在 data/system/packages.xml 中查看。

那么,這個 primaryCpuAbi 屬性值是如何影響應用進程是 32 位還是 64 位的呢?

這就涉及到 Zygote 方面的知識了。

在系統啟動之后,系統會根據設備的 ro.zygote 屬性值決定啟動哪個 Zygote,可以通過執行 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 發消息過來啟動新的應用時 fork 當前 Zygote 進程,所以,如果 AMS 是發給 64 位的 Zygote,那么新的應用自然就是跑在 64 位的進程上;同理,如果發給了 32 位的 Zygote 進程,那么 fork 出來的進程自然也就是 32 位的。

那么,可以跟隨着 AMS 的 startProcessLocked() 方法,去看看是以什么為依據選擇 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 開發人員寫測試用例用的吧,或者其他一些場景
    String requiredAbi = (abiOverride != null) ? abiOverride: app.info.primaryCpuAbi;
    if (requiredAbi == null) {
        //2. 如果為空,以設備支持的首個 abi 屬性值,可執行 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 的相關信息怎么來的,跟上一小節一樣,解析 Intent 時交由 PKMS 去它模塊內部的 Settings 讀取的。

如果 primaryCpuAbi 為空,則以設備支持的首個 abi 屬性值為主,設備支持的 abi 列表可以通過執行 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 位?

還記得上述說過的,系統啟動后,會去根據設備的 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,可執行文件位於 /system/bin/app_process64,后面是參數以及其他命令。

所以,名為 zygote 和 zygote_secondary 分別對應的是哪個 Zygote 進程,就取決於設備的 ro.zygote 屬性。

而,獲取 Zygote 支持的 abi 列表是通過 ZygoteState 的 connect() 方法,我們繼續跟進看看:

//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(",")));
}

發現沒有,源碼內部將 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

系統啟動后,Zygote 工作的相關內容基本都打日志出來了。

最后,再來稍微理一理:

app 安裝過程,會確定 app 的一個屬性值:primaryCpuAbi,它代表着這個應用的 so 文件使用的是哪個 abi 架構,而且它的確定過程很復雜,apk 包中的 so 文件、系統應用、相同 UID 的應用、設備的 abilist 等都對這個屬性值的確定過程有所影響。安裝成功后,可以在 data/system/packages.xml 中查看這個屬性值。

每啟動一個新的應用,都是運行在新的進程中,而新的進程是從 Zygote 進程 fork 過來的,系統在啟動時,會根據設備的 ro.zygote 屬性值決定啟動哪幾個 Zygote 進程,然后打開 socket,等待 AMS 發送消息來 fork 新進程。

當系統要啟動一個新的應用時,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 列表,系統都有打印相關日志,可過濾 Zygote 查看,如沒發現,可重啟設備查看。

abi 兼容

so 文件加載的流程,及應用運行在 32 位或 64 位的依據我們都梳理完了,以上內容足夠掌握什么場景下,該去哪些目錄下加載 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

你們如果去網上搜索如下關鍵字:so 文件,abi 兼容等,你們會發現,蠻多文章里都會說: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 位優化過的性能(ART,webview,media等等 )。這個意思就是說,程序啟動時是從 32 位的 Zygote 進程 fork 過來的,等於你在 64 位的設備上,但卻只運行在 32 位的進程上。

至於程序如何決定運行在 32 位還是 64 位,上面的章節中也分析過了,以 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 信息,當發現,如果當前進程運行在 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 文件都解壓出來,只會解壓一種,因為應用在安裝過程中,系統已經確定你這個應用是應該運行在 64 位還是 32 位的進程上了,並將這個結果保存在 app 的 primaryCpuAbi 屬性值中。

既然系統已經明確你的應用所運行的進程是 32 位還是 64 位,那么只需拷貝對應的一份 so 文件即可,畢竟 64 位的 so 文件和 32 位的又不能混合使用。

以上,是我的理解,如果有誤,歡迎指點下。

總結

整篇梳理下來,雖然梳理 so 的加載流程不難,但要掌握知其所以然的程度,就需要多花費一點心思了。

畢竟都涉及到應用的安裝機制,應用啟動流程,系統啟動機制,Zygote 相關的知識點了。如果你是開發系統應用的,建議還是花時間整篇看一下,畢竟系統應用的集成不像三方應用那樣在 apk 安裝期間自動將相關 so 文件解壓到 nativeLibraryDirectories 路徑下了。三方應用很少需要了解 so 的加載流程,但開發系統應用還是清楚點比較好。

不管怎么說,有時間,可以稍微跟着過一下整篇,相信多少是會有些收獲的,如果發現哪里有誤,也歡迎指點。沒時間的話,那就看看總結吧。

  • 一個應用在安裝過程中,系統會經過一系列復雜的邏輯確定兩個跟 so 文件加載相關的 app 屬性值:nativeLibraryDirectories ,primaryCpuAbi ;
  • nativeLibraryDirectories 表示應用自身存放 so 文件的目錄地址,影響着 so 文件的加載流程;
  • primaryCpuAbi 表示應用應該運行在哪種 abi 上,如(armeabi-v7a),它影響着應用是運行在 32 位還是 64 位的進程上,進而影響到尋找系統指定的 so 文件目錄的流程;
  • 以上兩個屬性,在應用安裝結束后,可在 data/system/packages.xml 中查看;
  • 當調用 System 的 loadLibrary() 加載 so 文件時,流程如下:
  • 先到 nativeLibraryDirectories 指向的目錄中尋找,是否存在且可用的 so 文件,有則直接加載這里的 so 文件;
  • 上一步沒找到的話,則根據當前進程如果是 32 位的,那么依次去 vendor/lib 和 system/lib 目錄中尋找;
  • 同樣,如果當前進程是 64 位的,那么依次去 vendor/lib64 和 system/lib64 目錄中尋找;
  • 當前應用是運行在 32 位還是 64 位的進程上,取決於系統的 ro.zygote 屬性和應用的 primaryCpuAbi 屬性值,系統的 ro.zygote 可通過執行 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 -- 系統進程Zygote的啟動分析

2.Android應用程序進程啟動過程(前篇)

3.如何查找native方法

4.Android中app進程ABI確定過程

5.Android 64 bit SO加載機制


大家好,我是 dasu,歡迎關注我的公眾號(dasuAndroidTv),如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支持~
dasuAndroidTv2.png


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM