动态链接库加载原理及HotFix方案

随着项目中动态链接库越来越多,我们遇到了很多奇怪的问题,比如只在某一种OS上会出现的java.lang.UnsatisfiedLinkError,但是明明我们动态库名称没错,abi也没错,方法也能对应的上,而且还只出现在某一些机型上,搞的我们百思不得其解。为了找到出现千奇百怪问题的原因,和能够提供一个方式来解决一些比较奇怪的动态库加载的问题,我发现了解一下so的加载流程是非常有必要的了,便于我们发现问题和解决问题,这就是本文的由来。

要想了解动态链接库是如何加载的,首先是查看动态链接库是怎么加载的,从我们日常调用的System.loadLibrary开始。为了书写方便,后文中会用“so“来简单替代“动态链接库”概念。

1、动态链接库的加载流程

首先从宏观流程上来看,对于load过程我们分为find&load,首先是要找到so所在的位置,然后才是load加载进内存,同时对于dalvik和art虚拟机来说,他们加载so的流程和方式也不尽相同,考虑到历史的进程我们分析art虚拟机的加载方式,先贴一张图看看so加载的大概流程。


我的疑问


  • ClassLoader是如何去找到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的地址


    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) + "\"");
            }

如果这里没有找到就要抛出来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的时候就会失败报错。

6、如何判断这个so是否加载过

我们前面说的都是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对象,这个结论对我们后文的解决方案很有帮助。

7、解决方案

那么你说了这么多,应该怎么解决呢?

其实看了这么多代码,熟悉hotpatch的同学应该要说了,哎呀这个和java层的patch逻辑好像啊,只不过java层的patch是插入dex数组,咱们这个是插入到nativeLibraryDirectory数组中,通过这样类似的方式就能动态patch修复这个问题了。

其实本质的原理和java层的patch是类似的,由于动态链接库组件已经在公司内开源了,所以这里就不贴的代码了,这里说几个需要注意的点:

1、如果是abi导致拷贝不全的问题不一定需要patch,可以自己解析一遍安装的apk做一次完整so拷贝,来插入到nativeLibraryDirectory的末尾,以此来保证so都能找到。

2、在拷贝so的时候要保证优先拷贝primaryCpuAbi的so

3、解决拷贝时机问题,在某些机型上如果程序一起来就挂,你连拷贝的时机都没有了

4、可以通过patch包来动态决定primaryCpuAbi的问题,解决一些app和so位数不一致的问题。patch包解压后的地址需要插入到nativeLibraryDirectory的数组首位,从而使得程序的位数和so的位数兼容。

对于hotfix方案有更多细节想要了解欢迎留言讨论。

编辑于 2016-08-21

文章被以下专栏收录