本文來自於騰訊bugly開發者社區,非經做者贊成,請勿轉載,原文地址:http://dev.qq.com/topic/57bec216d81f2415515d3e9cjava
做者:陳昱全node
隨着項目中動態連接庫愈來愈多,咱們也遇到了不少奇怪的問題,好比只在某一種 OS 上會出現的 java.lang.UnsatisfiedLinkError
,可是明明咱們動態庫名稱沒錯,ABI 也沒錯,方法也能對應的上,並且還只出如今某一些機型上,搞的咱們百思不得其解。爲了找到出現千奇百怪問題的緣由,和可以提供一個方式來解決一些比較奇怪的動態庫加載的問題,我發現瞭解一下 so 的加載流程是很是有必要的了,便於咱們發現問題和解決問題,這就是本文的由來。數組
要想了解動態連接庫是如何加載的,首先是查看動態連接庫是怎麼加載的,從咱們平常調用的 System.loadLibrary
開始。微信
爲了書寫方便,後文中會用「so」來簡單替代「動態連接庫」概念。app
首先從宏觀流程上來看,對於 load 過程咱們分爲 find&load,首先是要找到 so 所在的位置,而後纔是 load 加載進內存,同時對於 dalvik 和 art 虛擬機來講,他們加載 so 的流程和方式也不盡相同,考慮到歷史的進程咱們分析 art 虛擬機的加載方式,先貼一張圖看看 so 加載的大概流程。less
個人疑問socket
找到以上的幾個問題的答案,能夠幫咱們瞭解到哪一個步驟沒有找到動態連接庫,是由於名字不對,仍是 app 安裝後沒有拷貝過來動態連接庫仍是其餘緣由等,咱們先從第一個問題來了解。ide
首先咱們從調用源碼看起,瞭解 System.loadLibrary
是如何去找到 so 的。函數
System.java工具
public void loadLibrary(String nickname) { loadLibrary(nickname, VMStack.getCallingClassLoader()); }
經過 ClassLoader 的 findLibaray 來找到 so 的地址
void loadLibrary(String libraryName, ClassLoader loader) { if (loader != null) { String filename = loader.findLibrary(libraryName); if (filename == null) { // It's not necessarily true that the ClassLoader used // System.mapLibraryName, but the default setup does, and it's // misleading to say we didn't find "libMyLibrary.so" when we // actually searched for "liblibMyLibrary.so.so". throw new UnsatisfiedLinkError(loader + " couldn't find \"" + System.mapLibraryName(libraryName) + "\""); } String error = doLoad(filename, loader); if (error != null) { throw new UnsatisfiedLinkError(error); } return; }
若是這裏沒有找到就要拋出來 so 沒有找到的錯誤了,這個也是咱們很是常見的錯誤。因此這裏咱們很須要知道這個 ClassLoader 是哪裏來的。
這裏的一切都要比較熟悉 app 的啓動流程,關於 app 啓動的流程網上已經說過不少了,我就再也不詳細說了,一個 app 的啓動入口是在 ActivityThread 的 main 函數裏,這裏啓動了咱們的 UI 線程,最終啓動流程會走到咱們在 ActivityThread 的 handleBindApplication 函數中。
private void handleBindApplication(AppBindData data) { ...... ...... ContextImpl instrContext = ContextImpl.createAppContext(this, pi); try { java.lang.ClassLoader cl = instrContext.getClassLoader(); mInstrumentation = (Instrumentation) cl.loadClass(data.instrumentationName.getClassName()).newInstance(); } catch (Exception e) { throw new RuntimeException( "Unable to instantiate instrumentation " + data.instrumentationName + ": " + e.toString(), e); } mInstrumentation.init(this, instrContext, appContext, new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher, data.instrumentationUiAutomationConnection); ...... ...... } finally { StrictMode.setThreadPolicy(savedPolicy); } }
咱們找到了這個 classLoader 是從 ContextImpl 中拿過來的,有興趣的同窗能夠一步步看看代碼,最後的初始化實際上是在 ApplicationLoaders 的 getClassLoader 中
ApplicationLoaders.java
public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent) { ...... ...... if (parent == baseParent) { ClassLoader loader = mLoaders.get(zip); if (loader != null) { return loader; } Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip); PathClassLoader pathClassloader = new PathClassLoader(zip, libPath, parent); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); mLoaders.put(zip, pathClassloader); return pathClassloader; } Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip); PathClassLoader pathClassloader = new PathClassLoader(zip, parent); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); return pathClassloader; } }
實際上是一個 PathClassLoader,他的基類是 BaseDexClassLoader,在他其中的實現了咱們上文看到的 findLibrary 這個函數,經過 DexPathList 去 findLibrary。
BaseDexClassLoader.java
public String findLibrary(String libraryName) { String fileName = System.mapLibraryName(libraryName); for (File directory : nativeLibraryDirectories) { File file = new File(directory, fileName); if (file.exists() && file.isFile() && file.canRead()) { return file.getPath(); } } return null; }
代碼的意思很簡單,其實就是首先給 so 拼成完整的名字好比a拼接成 liba.so 這樣,而後再從存放 so 的文件夾中找這個 so,在哪一個文件夾裏面找到了,咱們就返回他的絕對路徑。因此這裏最關鍵的就是如何知道這個 nativeLibraryDirectories 的值是多少,因而也引出咱們下一個疑問, native 地址庫是怎麼來的,是多少呢?
經過查看 DexPathList 能夠知道,這個 nativeLibraryDirectories 的值來自於2個方面,一個是來自外部傳過來的 libraryPath,一個是來自 java.library.path
這個環境變量的值。
DexPathList.java
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 * * 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()]); }
環境變量的值你們 getProp 一下就知道是什麼值了,通常來講你們在 so 找不到的狀況下能看到這個環境變量的值,好比大部分只支持32位的系統狀況是這個:「/vendor/lib,/system/lib」,搞清楚了這個環境變量,重點仍是要知道這個 libraryPath 是如何來的,還記得咱們前面講了 ClassLoader 是如何來的嗎,其實在初始化 ClassLoader 的時候從外面告訴了 Loader 這個文件夾的地址是哪裏來的,在 LoadedApk 的 getClassLoader 代碼中咱們發現了主要是 libPath 這個 list 的 path 組成的,而這個 list 的組成主要來自下面2個地方:
LoadedApk.java
libPaths.add(mLibDir);
還有一個
// Add path to libraries in apk for current abi if (mApplicationInfo.primaryCpuAbi != null) { for (String apk : apkPaths) { libPaths.add(apk + "!/lib/" + mApplicationInfo.primaryCpuAbi); } }
這個 apkPath 大部分狀況都會是 apk 的安裝路徑,對於用戶的 app 大部分路徑都是在 /data/app 下,因此咱們要確認如下2個關鍵的值是怎麼來的,一個是 mLibDir,另一個就是這個 primaryCpuAbi 的值。
首先咱們來看看這個 mLibDir 是怎麼來的,經過觀察代碼咱們瞭解到這個 mLibDir 其實就是 ApplicationInfo 裏面 nativeLibraryDir 來的,那麼這個 nativeLibraryDir 又是如何來的呢,這個咱們還得從 App 安裝提及了,因爲本文的重點是講述 so 的加載,因此這裏不細說 App 安裝的細節了,我這裏重點列一下這個 nativeLibraryDir 是怎麼來的。
不管是替換仍是新安裝,都會調用 PackageManagerService 的 scanPackageLI 函數,而後跑去 scanPackageDirtyLI,在 scanPackageDirtyLI 這個函數上,咱們能夠找到這個設置 nativeLibraryDir 的邏輯。
PackageManagerService.java
// Give ourselves some initial paths; we'll come back for another // pass once we've determined ABI below. setNativeLibraryPaths(pkg);
info.nativeLibraryDir = null; info.secondaryNativeLibraryDir = null; if (isApkFile(codeFile)) { // Monolithic install ...... ...... final String apkName = deriveCodePathName(codePath); info.nativeLibraryRootDir = new File(mAppLib32InstallDir, apkName) .getAbsolutePath(); } info.nativeLibraryRootRequiresIsa = false; info.nativeLibraryDir = info.nativeLibraryRootDir;
static String deriveCodePathName(String codePath) { if (codePath == null) { return null; } final File codeFile = new File(codePath); final String name = codeFile.getName(); if (codeFile.isDirectory()) { return name; } else if (name.endsWith(".apk") || name.endsWith(".tmp")) { final int lastDot = name.lastIndexOf('.'); return name.substring(0, lastDot); } else { Slog.w(TAG, "Odd, " + codePath + " doesn't look like an APK"); return null; } }
apkName 主要是來自於這個 codePath,codePath 通常都是app的安裝地址,相似於:/data/app/com.test-1.apk 這樣的文件格式,若是是以.apk 結尾的狀況,這個 apkName 其實就是 com.test-1 這個名稱。
pkg.codePath = packageDir.getAbsolutePath();
而 nativeLibraryRootDir 的值就是 app native 庫的路徑這個的初始化主要是在 PackageManagerService 的構造函數中
mAppLib32InstallDir = new File(dataDir, "app-lib");
綜合上面的邏輯,連在一塊兒就能夠獲得這個 libPath 的地址,好比對於 com.test 這個包的 app,最後的 nativeLibraryRootDir 其實就是 /data/app-lib/com.test-1 這個路徑下,你其實能夠從這個路徑下找到你的 so 庫。
首先解釋下 Abi 的概念:
應用程序二進制接口(application binary interface,ABI) 描述了應用程序和操做系統之間,一個應用和它的庫之間,或者應用的組成部分之間的低接口 。ABI 不一樣於 API ,API 定義了源代碼和庫之間的接口,所以一樣的代碼能夠在支持這個 API 的任何系統中編譯 ,然而 ABI 容許編譯好的目標代碼在使用兼容 ABI 的系統中無需改動就能運行。
而爲何有 primaryCpuAbi 的概念呢,由於一個系統支持的 abi 有不少,不止一個,好比一個64位的機器上他的 supportAbiList 可能以下所示
public static final String[] SUPPORTED_ABIS = getStringList("ro.product.cpu.abilist", ",");
root@:/ # getprop ro.product.cpu.abilist arm64-v8a,armeabi-v7a,armeabi
因此他能支持的 abi 有如上的三個,這個 primaryCpuAbi 就是要知道當前程序的 abi 在他支持的 abi 中最靠前的那一個, 這個邏輯咱們要放在 so copy 的邏輯一塊兒講,由於在 so copy 的時候會決定 primaryCpuAbi,同時依靠這個 primaryCpuAbi 的值來決定咱們的程序是運行在32位仍是64位下的。
這裏總結一下,這個 libraryPath 主要來自兩個方向:一個是 data 目錄下 app-lib 中安裝包目錄,好比:/data/app-lib/com.test-1,另外一個方向就是來自於 apkpath+"!/lib/"+primaryCpuAbi 的地址了,好比:/data/app/com.test-1.apk!/lib/arm64-v8a。
這下咱們基本瞭解清楚了系統會從哪些目錄下去找這個 so 的值了:一個是系統配置設置的值,這個主要針對的是系統 so 的路徑,另一個就是 /data/app-lib 下和 /data/app apk 的安裝目錄下對應的 abi 目錄下去找。
另外不一樣的系統這些默認的 apkPath 和 codePath 可能會不同,要想知道最精確的值,能夠在你的 so 找不到的時候輸出的日誌中找到這個 so 的路徑,好比6.0的機器上的路徑又是這樣的:
nativeLibraryDirectories=[/data/app/com.qq.qcloud-1/lib/arm, /data/app/com.qq.qcloud-1/base.apk!/lib/armeabi-v7a, /vendor/lib, /system/lib]]]
瞭解了咱們去哪找,若是找不到的話那就只有2個狀況了,一個是好比 abi 對應錯了,另外就是是否是系統在安裝的時候沒有正常的將 so 拷貝這些路徑下,致使了找不到的狀況呢?因此咱們仍是須要了解在安裝的時候這些 so 是如何拷貝到正常的路徑下的,中間是否是會出一些問題呢?
關於 so 的拷貝咱們仍是照舊不細說 App 的安裝流程了,主要仍是和以前同樣不管是替換仍是新安裝,都會調用 PackageManagerService 的 scanPackageLI() 函數,而後跑去 scanPackageDirtyLI 函數,而在這個函數中對於非系統的 APP 他調用了 derivePackageABI 這個函數,經過這個函數他將會以爲系統的abi是多少,而且也會進行咱們最關心的 so 拷貝操做。
PackageManagerService.java
public void derivePackageAbi(PackageParser.Package pkg, File scanFile, String cpuAbiOverride, boolean extractLibs) throws PackageManagerException { ...... ...... if (isMultiArch(pkg.applicationInfo)) { // Warn if we've set an abiOverride for multi-lib packages.. // By definition, we need to copy both 32 and 64 bit libraries for // such packages. if (pkg.cpuAbiOverride != null && !NativeLibraryHelper.CLEAR_ABI_OVERRIDE.equals(pkg.cpuAbiOverride)) { Slog.w(TAG, "Ignoring abiOverride for multi arch application."); } int abi32 = PackageManager.NO_NATIVE_LIBRARIES; int abi64 = PackageManager.NO_NATIVE_LIBRARIES; if (Build.SUPPORTED_32_BIT_ABIS.length > 0) { if (extractLibs) { abi32 = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle, nativeLibraryRoot, Build.SUPPORTED_32_BIT_ABIS, useIsaSpecificSubdirs); } else { abi32 = NativeLibraryHelper.findSupportedAbi(handle, Build.SUPPORTED_32_BIT_ABIS); } } maybeThrowExceptionForMultiArchCopy( "Error unpackaging 32 bit native libs for multiarch app.", abi32); if (Build.SUPPORTED_64_BIT_ABIS.length > 0) { if (extractLibs) { abi64 = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle, nativeLibraryRoot, Build.SUPPORTED_64_BIT_ABIS, useIsaSpecificSubdirs); } else { abi64 = NativeLibraryHelper.findSupportedAbi(handle, Build.SUPPORTED_64_BIT_ABIS); } } maybeThrowExceptionForMultiArchCopy( "Error unpackaging 64 bit native libs for multiarch app.", abi64); if (abi64 >= 0) { pkg.applicationInfo.primaryCpuAbi = Build.SUPPORTED_64_BIT_ABIS[abi64]; } if (abi32 >= 0) { final String abi = Build.SUPPORTED_32_BIT_ABIS[abi32]; if (abi64 >= 0) { pkg.applicationInfo.secondaryCpuAbi = abi; } else { pkg.applicationInfo.primaryCpuAbi = abi; } } } else { String[] abiList = (cpuAbiOverride != null) ? new String[] { cpuAbiOverride } : Build.SUPPORTED_ABIS; // Enable gross and lame hacks for apps that are built with old // SDK tools. We must scan their APKs for renderscript bitcode and // not launch them if it's present. Don't bother checking on devices // that don't have 64 bit support. boolean needsRenderScriptOverride = false; if (Build.SUPPORTED_64_BIT_ABIS.length > 0 && cpuAbiOverride == null && NativeLibraryHelper.hasRenderscriptBitcode(handle)) { abiList = Build.SUPPORTED_32_BIT_ABIS; needsRenderScriptOverride = true; } final int copyRet; if (extractLibs) { copyRet = NativeLibraryHelper.copyNativeBinariesForSupportedAbi(handle, nativeLibraryRoot, abiList, useIsaSpecificSubdirs); } else { copyRet = NativeLibraryHelper.findSupportedAbi(handle, abiList); } if (copyRet < 0 && copyRet != PackageManager.NO_NATIVE_LIBRARIES) { throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Error unpackaging native libs for app, errorCode=" + copyRet); } if (copyRet >= 0) { pkg.applicationInfo.primaryCpuAbi = abiList[copyRet]; } else if (copyRet == PackageManager.NO_NATIVE_LIBRARIES && cpuAbiOverride != null) { pkg.applicationInfo.primaryCpuAbi = cpuAbiOverride; } else if (needsRenderScriptOverride) { pkg.applicationInfo.primaryCpuAbi = abiList[0]; } } } catch (IOException ioe) { Slog.e(TAG, "Unable to get canonical file " + ioe.toString()); } finally { IoUtils.closeQuietly(handle); } // Now that we've calculated the ABIs and determined if it's an internal app, // we will go ahead and populate the nativeLibraryPath. setNativeLibraryPaths(pkg); }
流程大體以下,這裏的 nativeLibraryRoot 其實就是咱們上文提到過的 mLibDir,這樣就完成了咱們的對應關係,咱們要從 apk 中解壓出 so,而後拷貝到 mLibDir 下,這樣在 load 的時候才能去這裏找的到這個文件,這個值咱們舉個簡單的例子方便理解,好比 com.test 的 app,這個 nativeLibraryRoot 的值基本能夠理解成:/data/app-lib/com.test-1。
接下來的重點就是查看這個拷貝邏輯是如何實現的,代碼在 NativeLibraryHelper 中 copyNativeBinariesForSupportedAbi 的實現
public static int copyNativeBinariesForSupportedAbi(Handle handle, File libraryRoot, String[] abiList, boolean useIsaSubdir) throws IOException { createNativeLibrarySubdir(libraryRoot); /* * If this is an internal application or our nativeLibraryPath points to * the app-lib directory, unpack the libraries if necessary. */ int abi = findSupportedAbi(handle, abiList); if (abi >= 0) { /* * If we have a matching instruction set, construct a subdir under the native * library root that corresponds to this instruction set. */ final String instructionSet = VMRuntime.getInstructionSet(abiList[abi]); final File subDir; if (useIsaSubdir) { final File isaSubdir = new File(libraryRoot, instructionSet); createNativeLibrarySubdir(isaSubdir); subDir = isaSubdir; } else { subDir = libraryRoot; } int copyRet = copyNativeBinaries(handle, subDir, abiList[abi]); if (copyRet != PackageManager.INSTALL_SUCCEEDED) { return copyRet; } } return abi; }
函數 copyNativeBinariesForSupportedAbi,他的核心業務代碼都在 native 層,它主要作了以下的工做
這個 nativeLibraryRootDir 上文在說到去哪找 so 的時候提到過了,實際上是在這裏建立的,而後咱們重點看看 findSupportedAbi 和 copyNativeBinaries 的邏輯。
findSupportedAbi 函數其實就是遍歷 apk(其實就是一個壓縮文件)中的全部文件,若是文件全路徑中包含 abilist 中的某個 abi 字符串,則記錄該 abi 字符串的索引,最終返回全部記錄索引中最靠前的,即排在 abilist 中最前面的索引。
這裏的abi用來決定咱們是32位仍是64位,對於既有32位也有64位的狀況,咱們會採用64位,而對於僅有32位或者64位的話就認爲他是對應的位數下,僅有32位就是32位,僅有64位就認爲是64位的。
當前文肯定好是用32位仍是64位後,咱們就會取出來對應的上文查找到的這個 abi 值,做爲 primaryCpuAbi。
這個 primaryCpuAbi 的值是安裝的時候持久化在 pkg.applicationInfo 中的,因此一旦 abi 致使進程位數出錯或者 primaryCpuAbi 出錯,就可能會致使一直出錯,重啓也沒有辦法修復,須要咱們用一些 hack 手段來進行修復。
NativeLibraryHelper 中的 findSupportedAbi 核心代碼主要以下,基本就是咱們前文說的主要邏輯,遍歷 apk(其實就是一個壓縮文件)中的全部文件,若是文件全路徑中包含 abilist 中的某個 abi 字符串,則記錄該 abi 字符串的索引,最終返回全部記錄索引中最靠前的,即排在 abilist 中最前面的索引
NativeLibraryHelper.cpp
UniquePtr<NativeLibrariesIterator> it(NativeLibrariesIterator::create(zipFile)); if (it.get() == NULL) { return INSTALL_FAILED_INVALID_APK; } ZipEntryRO entry = NULL; int status = NO_NATIVE_LIBRARIES; while ((entry = it->next()) != NULL) { // We're currently in the lib/ directory of the APK, so it does have some native // code. We should return INSTALL_FAILED_NO_MATCHING_ABIS if none of the // libraries match. if (status == NO_NATIVE_LIBRARIES) { status = INSTALL_FAILED_NO_MATCHING_ABIS; } const char* fileName = it->currentEntry(); const char* lastSlash = it->lastSlash(); // Check to see if this CPU ABI matches what we are looking for. const char* abiOffset = fileName + APK_LIB_LEN; const size_t abiSize = lastSlash - abiOffset; for (int i = 0; i < numAbis; i++) { const ScopedUtfChars* abi = supportedAbis[i]; if (abi->size() == abiSize && !strncmp(abiOffset, abi->c_str(), abiSize)) { // The entry that comes in first (i.e. with a lower index) has the higher priority. if (((i < status) && (status >= 0)) || (status < 0) ) { status = i; } } } }
舉個例子,加入咱們的 app 中的 so 地址中有包含 arm64-v8a 的字符串,同時 abilist 是 arm64-v8a,armeabi-v7a,armeab,那麼這裏就會返回 arm64-v8a。這裏其實須要特別注意,返回的是第一個,這裏極可能會形成一些 so 位數不一樣,致使運行錯誤以及 so 找不到的狀況。 具體咱們還要結合 so 的 copy 來一塊兒闡述。
主要的代碼邏輯也是在 NativeLibraryHelper.cpp 中的 iterateOverNativeFiles 函數中,核心代碼以下:
NativeLibraryHelper.cpp
if (cpuAbi.size() == cpuAbiRegionSize && *(cpuAbiOffset + cpuAbi.size()) == '/' && !strncmp(cpuAbiOffset, cpuAbi.c_str(), cpuAbiRegionSize)) { ALOGV("Using primary ABI %s\n", cpuAbi.c_str()); hasPrimaryAbi = true; } else if (cpuAbi2.size() == cpuAbiRegionSize && *(cpuAbiOffset + cpuAbi2.size()) == '/' && !strncmp(cpuAbiOffset, cpuAbi2.c_str(), cpuAbiRegionSize)) { /* * If this library matches both the primary and secondary ABIs, * only use the primary ABI. */ if (hasPrimaryAbi) { ALOGV("Already saw primary ABI, skipping secondary ABI %s\n", cpuAbi2.c_str()); continue; } else { ALOGV("Using secondary ABI %s\n", cpuAbi2.c_str()); } } else { ALOGV("abi didn't match anything: %s (end at %zd)\n", cpuAbiOffset, cpuAbiRegionSize); continue; } // If this is a .so file, check to see if we need to copy it. if ((!strncmp(fileName + fileNameLen - LIB_SUFFIX_LEN, LIB_SUFFIX, LIB_SUFFIX_LEN) && !strncmp(lastSlash, LIB_PREFIX, LIB_PREFIX_LEN) && isFilenameSafe(lastSlash + 1)) || !strncmp(lastSlash + 1, GDBSERVER, GDBSERVER_LEN)) { install_status_t ret = callFunc(env, callArg, &zipFile, entry, lastSlash + 1); if (ret != INSTALL_SUCCEEDED) { ALOGV("Failure for entry %s", lastSlash + 1); return ret; } } }
主要的策略就是,遍歷 apk 中文件,當遍歷到有主 Abi 目錄的 so 時,拷貝並設置標記 hasPrimaryAbi 爲真,之後遍歷則只拷貝主 Abi 目錄下的 so。這個主 Abi 就是咱們前面 findSupportedAbi 的時候找到的那個 abi 的值,你們能夠去回顧下。
當標記爲假的時候,若是遍歷的 so 的 entry 名包含其餘abi字符串,則拷貝該 so,拷貝 so 到咱們上文說到 mLibDir 這個目錄下。
這裏有一個很重要的策略是:ZipFileRO 的遍歷順序,他是根據文件對應 ZipFileR0 中的 hash 值而定,而對於已經 hasPrimaryAbi 的狀況下,非 PrimaryAbi 是直接跳過 copy 操做的,因此這裏可能會出現不少拷貝 so 失敗的狀況。
舉個例子:假設存在這樣的 apk, lib 目錄下存在 armeabi/libx.so , armeabi/liby.so , armeabi-v7a/libx.so 這三個 so 文件,且 hash 的順序爲 armeabi-v7a/libx.so 在 armeabi/liby.so 以前,則 apk 安裝的時候 liby.so 根本不會被拷貝,由於按照拷貝策略, armeabi-v7a/libx.so 會優先遍歷到,因爲它是主 abi 目錄的 so 文件,因此標記被設置了,當遍歷到 armeabi/liby.so 時,因爲標記被設置爲真, liby.so 的拷貝就被忽略了,從而在加載 liby.so 的時候會報異常。
Android 在5.0之後其實已經支持64位了,而對於不少時候你們在運行so的時候也會遇到這樣的錯誤:dlopen failed: "xx.so" is 32-bit instead of 64-bit,這種狀況實際上是由於進程由 64zygote 進程 fork 出來,在64位的進程上必需要64位的動態連接庫。
Art 上支持64位程序的主要策略就是區分了 zygote32 和 zygote64,對於32位的程序經過 zygote32 去 fork 而64位的天然是經過 zygote64去 fork。相關代碼主要在 ActivityManagerService 中:
ActivityManagerService.java
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);
Process.java
return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);
從代碼能夠看出,startProcessLocked 方法實現啓動應用,再經過 Process 中的 startViaZygote 方法,這個方法最終是向相應的 zygote 進程發出 fork 的請求 zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);
其中 openZygoteSocketIfNeeded(abi) 會根據 abi 的類型,選擇不一樣的 zygote 的 socket 監聽的端口,在以前的 init 文件中能夠看到,而這個 abi 就是咱們上文一直在提到的 primaryAbi。
因此當你的 app 中有64位的 abi,那麼就必須全部的 so 文件都有64位的,不能出現一部分64位的一部分32位的,當你的 app 發現 primaryAbi 是64位的時候,他就會經過 zygote64 fork 在64位下,那麼其餘的32位 so 在 dlopen 的時候就會失敗報錯。
咱們前面說的都是 so 是怎麼找的,哪裏找的,以及他又是如何拷貝到這裏來的,而咱們前面的大圖的流程有一個很明顯的流程就是找到後判斷已經加載過了,就不用再加載了。那麼是系統是依據什麼來判斷這個so已經加載過了呢,咱們要接着 System.java的doLoad 函數看起。
Runtime.java
private String doLoad(String name, ClassLoader loader) { String ldLibraryPath = null; if (loader != null && loader instanceof BaseDexClassLoader) { ldLibraryPath = ((BaseDexClassLoader) loader).getLdLibraryPath(); } // nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless // of how many ClassLoaders are in the system, but dalvik doesn't support synchronized // internal natives. synchronized (this) { return nativeLoad(name, loader, ldLibraryPath); } }
主要代碼在 nativeLoad 這裏作的,這裏再往下走是 native 方法了,因而咱們要走到 java_lang_runtime.cc 中去看這個 nativeLoad 的實現
java_lang_runtime.cc
static jstring Runtime_nativeLoad(JNIEnv* env, jclass, jstring javaFilename, jobject javaLoader, jstring javaLdLibraryPathJstr) { ScopedUtfChars filename(env, javaFilename); if (filename.c_str() == nullptr) { return nullptr; } SetLdLibraryPath(env, javaLdLibraryPathJstr); std::string error_msg; { JavaVMExt* vm = Runtime::Current()->GetJavaVM(); bool success = vm->LoadNativeLibrary(env, filename.c_str(), javaLoader, &error_msg); if (success) { return nullptr; } } // Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF. env->ExceptionClear(); return env->NewStringUTF(error_msg.c_str()); }
而後咱們發現核心在 JavaVMExt 中的 LoadNativeLibrary 函數實現的,因而咱們又去了解這個函數。
java_vm_ext.cc
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, const std::string& path, jobject class_loader, jstring library_path, std::string* error_msg) { error_msg->clear(); // See if we've already loaded this library. If we have, and the class loader // matches, return successfully without doing anything. // TODO: for better results we should canonicalize the pathname (or even compare // inodes). This implementation is fine if everybody is using System.loadLibrary. SharedLibrary* library; Thread* self = Thread::Current(); { // TODO: move the locking (and more of this logic) into Libraries. MutexLock mu(self, *Locks::jni_libraries_lock_); library = libraries_->Get(path); } void* class_loader_allocator = nullptr; { ScopedObjectAccess soa(env); // As the incoming class loader is reachable/alive during the call of this function, // it's okay to decode it without worrying about unexpectedly marking it alive. mirror::ClassLoader* loader = soa.Decode<mirror::ClassLoader*>(class_loader); ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); if (class_linker->IsBootClassLoader(soa, loader)) { loader = nullptr; class_loader = nullptr; } class_loader_allocator = class_linker->GetAllocatorForClassLoader(loader); CHECK(class_loader_allocator != nullptr); }
其實查找規則和他的註釋說的基本同樣,發現 so 的 path 同樣,而且關聯的 ClassLoader 也是一致的那麼就認爲這個 so 是已經加載過的什麼都不作,而這個 path 就是以前咱們 findLibrary 中找到 so 的絕對路徑。
因此若是要動態替換 so 的話,在已經加載過 so 的狀況下,有2個方式能夠再不重啓的狀況下就能作到 hotfix,要麼換 so 的 path,要麼就是改變 ClassLoader 對象,這個結論對咱們後文的解決方案頗有幫助。
那麼你說了這麼多,應該怎麼解決呢?
其實看了這麼多代碼,熟悉 hotpatch 的同窗應該要說了,哎呀這個和 java 層的 patch 邏輯好像啊,只不過 java 層的 patch 是插入 dex 數組,我們這個是插入到 nativeLibraryDirectory 數組中,經過這樣相似的方式就能動態 patch 修復這個問題了。
其實本質的原理和 java 層的 patch 是相似的,可是還有幾個點是須要注意的:
若是是 abi 致使拷貝不全的問題不必定須要 patch,能夠本身解析一遍安裝的 apk 作一次完整拷貝,來插入到 nativeLibraryDirectory 的末尾,以此來保證 so 都能找到。
在拷貝 so 的時候要保證優先拷貝 primaryCpuAbi 的 so
解決拷貝時機問題,在某些機型上若是程序一塊兒來就掛,你連拷貝的時機都沒有了
能夠經過 patch 包來動態決定 primaryCpuAbi 的問題,解決一些 app 和 so 位數不一致的問題。patch 包解壓後的地址須要插入到 nativeLibraryDirectory 的數組首位,從而使得程序的位數和 so 的位數兼容。
組件剛剛開發完成,還在驗證階段,回頭再放出來,幫助你們解決動態庫加載遇到的各類問題,之後媽媽不再用擔憂了 UnsatisfiedLinkError 的錯誤了。
你們能夠關注知乎帳號「陳昱全」,與我進行交流。
更多精彩內容歡迎關注bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!