最近碰到一些 so 文件問題,順便將相關知識點梳理一下。java
本文的結論是跟着 System.loadlibrary()
一層層源碼走進去,我的對其的理解所整理的,那麼開始看源碼以前,先來提幾個問題:android
Q1:你知道 so 文件的加載流程嗎?c++
Q2:設備存放 so 的路徑有 system/lib,vendor/lib,system/lib64,vendor/lib64,知道在哪裏規定了這些路徑嗎?清楚哪些場景下系統會去哪一個目錄下尋找 so 文件嗎?仍是說,全部的目錄都會去尋找?web
Q3:Zygote 進程是分 32 位和 64 位的,那麼,系統是如何決定某個應用應該運行在 32 位上,仍是 64 位上?數組
Q4:若是程序跑在 64 位的 Zygote 進程上時,可使用 32 位的 so 文件麼,即應用的 primaryCpuAbi 爲 arm64-v8a,那麼是否可以使用 armeabi-v7a 的 so 文件,兼容的嗎?緩存
Q2,Q3,Q4,這幾個問題都是基於設備支持 64 位的前提下,在舊系統版本中,只支持 32 位,也就沒這麼多疑問須要處理了。bash
因爲此次的源碼會涉及不少 framework 層的代碼,包括 java 和 c++,直接在 AndroidStudio 跟進 SDK 的源碼已不足夠查看到相關的代碼了。因此,這次是藉助 Source Insight 軟件,而源碼來源以下:架構
android.googlesource.com/platform/app
我並無將全部目錄下載下來,只下載了以下目錄的源碼:socket
我沒有下載最新版本的代碼,而是選擇了 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()
去加載。
但在深刻去探索以前,我想先探索另外一條分支,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
就是在這裏設置的。那麼,這個屬性值來源的邏輯是這樣的:
doLoad()
方法註釋中,Google 有解釋是說因爲 Android 的進程都是經過 Zygote 進程 fork 過來,因此不能使用 LD_LIBRARY_PATH 。應該,大概,多是這個意思吧,我英文不大好,大家能夠自行去確認一下。那麼,繼續看看 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 位進程的,那麼恰好能夠根據這個爲依據。
該走回主線了,在支線中的探索已經摸索了些經驗了。
大夥應該還記得吧,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 值是經過 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 文件的加載流程以下:
咱們已經清楚了,加載 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 進程關係以下:
而 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 查看,如沒發現,可重啓設備查看。
so 文件加載的流程,及應用運行在 32 位或 64 位的依據咱們都梳理完了,以上內容足夠掌握什麼場景下,該去哪些目錄下加載 so 文件的判斷能力了。
那麼,還有個問題,若是應用運行在 64 位上,那麼此時,它是否可以使用 armeabi-v7a 的 so 文件?
首先,先來羅列一下常見的 abi :
其中,運行在 64 位的 Zygote 進程上的是:
一樣,運行在 32 位的 Zygote 進程上的是:
大家若是去網上搜索以下關鍵字: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 的加載流程,但開發系統應用仍是清楚點比較好。
無論怎麼說,有時間,能夠稍微跟着過一下整篇,相信多少是會有些收穫的,若是發現哪裏有誤,也歡迎指點。沒時間的話,那就看看總結吧。
loadLibrary()
加載 so 文件時,流程以下:你們好,我是 dasu,歡迎關注個人公衆號(dasuAndroidTv),若是你以爲本篇內容有幫助到你,能夠轉載但記得要關注,要標明原文哦,謝謝支持~