【騰訊Bugly乾貨分享】動態鏈接庫載入原理及HotFix方案介紹

来源:http://www.cnblogs.com/bugly/archive/2016/08/26/5810732.html
-Advertisement-
Play Games

隨著項目中動態鏈接庫越來越多,我們也遇到了很多奇怪的問題,比如只在某一種 OS 上會出現的 `java.lang.UnsatisfiedLinkError`,但是明明我們動態庫名稱沒錯,ABI 也沒錯,方法也能對應的上,而且還只出現在某一些機型上,搞的我們百思不得其解。為了找到出現千奇百怪問題的原因... ...


本文來自於騰訊bugly開發者社區,非經作者同意,請勿轉載,原文地址:http://dev.qq.com/topic/57bec216d81f2415515d3e9c

作者:陳昱全

引言

隨著項目中動態鏈接庫越來越多,我們也遇到了很多奇怪的問題,比如只在某一種 OS 上會出現的 java.lang.UnsatisfiedLinkError,但是明明我們動態庫名稱沒錯,ABI 也沒錯,方法也能對應的上,而且還只出現在某一些機型上,搞的我們百思不得其解。為了找到出現千奇百怪問題的原因,和能夠提供一個方式來解決一些比較奇怪的動態庫載入的問題,我發現瞭解一下 so 的載入流程是非常有必要的了,便於我們發現問題和解決問題,這就是本文的由來。

要想瞭解動態鏈接庫是如何載入的,首先是查看動態鏈接庫是怎麼載入的,從我們日常調用的 System.loadLibrary 開始。

為了書寫方便,後文中會用“so”來簡單替代“動態鏈接庫”概念。

1、動態鏈接庫的載入流程

首先從巨集觀流程上來看,對於 load 過程我們分為 find&load,首先是要找到 so 所在的位置,然後才是 load 載入進記憶體,同時對於 dalvik 和 art 虛擬機來說,他們載入 so 的流程和方式也不盡相同,考慮到歷史的進程我們分析 art 虛擬機的載入方式,先貼一張圖看看 so 載入的大概流程。

我的疑問

  • ClassLoader 是如何去找到so的呢?
  • 如何判斷這個 so 是否載入過?
  • native 庫的地址是如何來的
  • so 是怎麼弄到 native 庫裡面去的?
  • 如何決定 app 進程是32位還是64位的?

找到以上的幾個問題的答案,可以幫我們瞭解到哪個步驟沒有找到動態鏈接庫,是因為名字不對,還是 app 安裝後沒有拷貝過來動態鏈接庫還是其他原因等,我們先從第一個問題來瞭解。

2、ClassLoader 如何找 so 呢?

首先我們從調用源碼看起,瞭解 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 是哪裡來的。

2.1 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 地址庫是怎麼來的,是多少呢?

3 nativeLibraryDirectories 是怎麼來的?

通過查看 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 的值。

3.1 mLibDir 是哪裡來的?

首先我們來看看這個 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 庫。

3.2 primaryCpuAbi 哪裡來的

首先解釋下 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位下的。

3.3 總結,我們是在哪些路徑下找的

這裡總結一下,這個 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 是如何拷貝到正常的路徑下的,中間是不是會出一些問題呢?

4、apk 安裝之—-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 的邏輯。

4.1 findSupportedAbi

findSupportedAbi 函數其實就是遍歷 apk(其實就是一個壓縮文件)中的所有文件,如果文件全路徑中包含 abilist 中的某個 abi 字元串,則記錄該 abi 字元串的索引,最終返回所有記錄索引中最靠前的,即排在 abilist 中最前面的索引。

4.1.1 32位還是64位

這裡的abi用來決定我們是32位還是64位,對於既有32位也有64位的情況,我們會採用64位,而對於僅有32位或者64位的話就認為他是對應的位數下,僅有32位就是32位,僅有64位就認為是64位的。

4.1.2 primaryCpuAbi 是多少

當前文確定好是用32位還是64位後,我們就會取出來對應的上文查找到的這個 abi 值,作為 primaryCpuAbi。

4.1.3 如果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 來一起闡述。

4.2 copyNativeBinaries

主要的代碼邏輯也是在 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 的時候會報異常。

5、64位的影響

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 的

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 原生js使用forEach()與jquery使用each()遍曆數組,return false 的區別: 1、使用each()遍曆數組a,如下: 結果如下: 從運行的效果可以看出,return 相當於迴圈中的break,直接結束整個迴圈。 2、使用forEach()遍曆數組a,如下: 結果如下: 從 ...
  • 《JavaScript權威指南》中指出:JavaScript變數在聲明之前已經可用,JavaScript的這個特性被非正式的稱為聲明提前(hoisting),即JavaScript函數中聲明的所有變數(但不涉及賦值)都被“提前”至函數的頂部。下麵我們從實例中看看: 實例1: 調用函數myFunc() ...
  • form 轉化為真正的數組 先說一下使用場景,在Js中,我們要經常操作DOM,比如獲取全部頁面的input標簽,並且找到類型為button的元素,然後給這個按鈕註冊一個點擊事件,我們可能會這樣操作; 這樣寫肯定是沒有問題的,但是我們知道很多操作數組的方法比for迴圈好用多了,比如es5的forEac ...
  • 概述 現在的開發工具基本都用AndroidStudio了。網上的開源框架也是。比如做瀑布式UI的StaggeredGridView,還有導航頁的PagerSlidingTabStrip等。 那麼電腦性能不好的,還在用eclipse怎麼使用這些開源框架呢? 步驟 準備工作 下載對應的框架如Stagge ...
  • UITextField游標消失 修改以下設置 改為 即可 ...
  • 1、重覆添加某個文件。解決辦法:搜索工程,刪除多餘的文件; 2、文件添加引用錯誤,即尾碼 .m 誤寫為 .h 。解決辦法:改正,編譯通過。 ...
  • Handoff簡介 Handoff是iOS 8 和 OS X v10.10中引入的功能,可以讓同一個用戶在多台設備間傳遞項目。In iOS 9 and OS X v10.11 支持了Spotlight中搜索並打開應用。 Handoff交互: 在iOS中這個user activity object是U ...
  • 蘋果提供的NSURLSessionDownloadTask雖然能實現斷點續傳,但是有些情況是無法處理的,比如程式強制退出或沒有調用 cancelByProducingResumeData取消方法,這時就無法斷點續傳了。 使用NSURLSession和NSURLSessionDataTask實現斷點續 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...