Android热修复及原理总结和介绍

热修复的产生原因

  • 刚发布的版本出现了bug,需要修复bug、测试并打包在各大应用市场重新发布上架。这样会耗费大量的人力和物力,代价比较大。
  • 已经修复了此前版本的bug,如果下一个版本是一个大版本,那么两个版本的发布时间间隔往往会较长,这样如果要等到大版本发布才能修复此前的bug,则bug就会长期地影响用户。
  • 版本升级率不高,并且需要很长时间来完成新版本的覆盖,此前版本的bug会一直影响还没有升级到新版本的用户。
  • 有一些重要的小功能,需要短时间内完成版本覆盖,比如节日活动。

以上情况下,就需要用到热修复来解决问题。

热修复框架的对比

  • 阿里系——AndFix、阿里百川、Sophix、Dexposed
  • 腾讯系——微信Tinker、QQ空间的超级补丁、手机QQ的Qfix
  • 知名公司——美团的Robust、饿了么的Amigo、美丽说蘑菇街的Aceso
  • 其他——Nuwa、RocooFix、AnoleFix

虽然市面上热修复框架很多,但热修复的核心技术主要是三种:资源修复、代码修复和动态链接库修复。其中每个核心技术又有各种不同的技术方案,各种技术方案又有不同的实现,而且很多热修复框架都还在持续的迭代和完善。总的来说,热修复框架的技术实现是繁多可变的,作为开发者,需要了解这些技术方案的基本原理。
热修复框架的对比

资源修复

很多热修复框架的资源修复实现都参考了Instant Run的资源修复的原理。

Instant Run

Instant Run是Android Studio 2.0以后新增的一种运行机制,能够显著减少开发者第二次以及以后的App构建和部署时间。

在没有Instant Run之前,我们编译部署App的流程如下图所示:
Instant Run出现之前
也就是说,每次修改代码或者资源之后,传统的编译部署都需要重新安装Apk和重启App。这样的机制,效率比较低,有重复不必要的耗时,Instant Run避免了这一情况。
Instant Run的运行机制

可以看出Instant Run的构建部署都是基于更改的部分。Instant Run的部署有三种方式,Instant Run会根据修改代码和资源的情况来决定用那种部署方式,无论那种方式,都不需要重新安装Apk。

  • Hot Swap——它是效率最高的部署方式,代码的增量修改不需要重启App,甚至不需要重启当前Activity。典型场景:修改了一个现有方法中的代码逻辑时采用Hot Swap。
  • Warm Swap——App不需要重启,但是当前的Activity需要重启。典型场景:修改或者删除了一个现有的资源文件时会采用Warm Swap。
  • Cold Swap——需要重启App,但不需要重新安装Apk。采用Cold Swap的情况很多,比如添加、删除或者修改了一个类的字段或方法,添加了一个类等。
    更多Intant Run原理相关的介绍,有兴趣的同学可以出门左转:深度理解Android InstantRun原理以及源码分析

Instant Run资源修复

既然很多热修复框架的的资源修复都是参考了Instant Run的资源修复原理,那我们了解Instant Run的资源修复原理就能够触类旁通了。Instant Run资源修复的核心逻辑在MonkeyPatcher的monkeyPatchExistingResources方法里面

public static void monkeyPatchExistingResources(Context context,
                                                String externalResourceFile, Collection activities) {
    if (externalResourceFile == null) {
        return;
    }
    try {
        //创建一个新的AssetManager对象
        AssetManager newAssetManager = (AssetManager) AssetManager.class
                .getConstructor(new Class[0]).newInstance(new Object[0]);
        Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
                "addAssetPath", new Class[] { String.class });
        mAddAssetPath.setAccessible(true);
        //通过反射调用mAddAssetPath方法,加载指定路径的资源文件
        if (((Integer) mAddAssetPath.invoke(newAssetManager,
                new Object[] { externalResourceFile })).intValue() == 0) {
            throw new IllegalStateException(
                    "Could not create new AssetManager");
        }
        Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod(
                "ensureStringBlocks", new Class[0]);
        mEnsureStringBlocks.setAccessible(true);
        mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
        if (activities != null) {
            for (Activity activity : activities) {
                            //遍历所有的Avitity对象,获取其Resources对象
                Resources resources = activity.getResources();
                try {
                    Field mAssets = Resources.class
                            .getDeclaredField("mAssets");
                    mAssets.setAccessible(true);
                    //将resources对象的mAssets字段设置为新创建的newAssetManager
                    mAssets.set(resources, newAssetManager);
                } catch (Throwable ignore) {
                    Field mResourcesImpl = Resources.class
                            .getDeclaredField("mResourcesImpl");
                    mResourcesImpl.setAccessible(true);
                    Object resourceImpl = mResourcesImpl.get(resources);
                    Field implAssets = resourceImpl.getClass()
                            .getDeclaredField("mAssets");
                    implAssets.setAccessible(true);
                    implAssets.set(resourceImpl, newAssetManager);
                }
                Resources.Theme theme = activity.getTheme();
                try {
                    try {
                        Field ma = Resources.Theme.class
                                .getDeclaredField("mAssets");
                        ma.setAccessible(true);
                        ma.set(theme, newAssetManager);
                    } catch (NoSuchFieldException ignore) {
                        Field themeField = Resources.Theme.class
                                .getDeclaredField("mThemeImpl");
                        themeField.setAccessible(true);
                        Object impl = themeField.get(theme);
                        Field ma = impl.getClass().getDeclaredField(
                                "mAssets");
                        ma.setAccessible(true);
                        ma.set(impl, newAssetManager);
                    }
                    Field mt = ContextThemeWrapper.class
                            .getDeclaredField("mTheme");
                    mt.setAccessible(true);
                    mt.set(activity, null);
                    Method mtm = ContextThemeWrapper.class
                            .getDeclaredMethod("initializeTheme",
                                    new Class[0]);
                    mtm.setAccessible(true);
                    mtm.invoke(activity, new Object[0]);
                    Method mCreateTheme = AssetManager.class
                            .getDeclaredMethod("createTheme", new Class[0]);
                    mCreateTheme.setAccessible(true);
                    Object internalTheme = mCreateTheme.invoke(
                            newAssetManager, new Object[0]);
                    Field mTheme = Resources.Theme.class
                            .getDeclaredField("mTheme");
                    mTheme.setAccessible(true);
                    mTheme.set(theme, internalTheme);
                } catch (Throwable e) {
                    Log.e("InstantRun",
                            "Failed to update existing theme for activity "
                                    + activity, e);
                }
                pruneResourceCaches(resources);
            }
        }
        Collection> references;
        if (Build.VERSION.SDK_INT >= 19) {
            Class resourcesManagerClass = Class
                    .forName("android.app.ResourcesManager");
            Method mGetInstance = resourcesManagerClass.getDeclaredMethod(
                    "getInstance", new Class[0]);
            mGetInstance.setAccessible(true);
            Object resourcesManager = mGetInstance.invoke(null,
                    new Object[0]);
            try {
                Field fMActiveResources = resourcesManagerClass
                        .getDeclaredField("mActiveResources");
                fMActiveResources.setAccessible(true);
                ArrayMap> arrayMap = (ArrayMap) fMActiveResources
                        .get(resourcesManager);
                references = arrayMap.values();
            } catch (NoSuchFieldException ignore) {
                Field mResourceReferences = resourcesManagerClass
                        .getDeclaredField("mResourceReferences");
                mResourceReferences.setAccessible(true);
                references = (Collection) mResourceReferences
                        .get(resourcesManager);
            }
        } else {
            Class activityThread = Class
                    .forName("android.app.ActivityThread");
            Field fMActiveResources = activityThread
                    .getDeclaredField("mActiveResources");
            fMActiveResources.setAccessible(true);
            Object thread = getActivityThread(context, activityThread);
            HashMap> map = (HashMap) fMActiveResources
                    .get(thread);
            references = map.values();
        }
        for (WeakReference wr : references) {
            Resources resources = (Resources) wr.get();
            if (resources != null) {
                try {
                    Field mAssets = Resources.class
                            .getDeclaredField("mAssets");
                    mAssets.setAccessible(true);
                    mAssets.set(resources, newAssetManager);
                } catch (Throwable ignore) {
                    Field mResourcesImpl = Resources.class
                            .getDeclaredField("mResourcesImpl");
                    mResourcesImpl.setAccessible(true);
                    Object resourceImpl = mResourcesImpl.get(resources);
                    Field implAssets = resourceImpl.getClass()
                            .getDeclaredField("mAssets");
                    implAssets.setAccessible(true);
                    implAssets.set(resourceImpl, newAssetManager);
                }
                resources.updateConfiguration(resources.getConfiguration(),
                        resources.getDisplayMetrics());
            }
        }
    } catch (Throwable e) {
        throw new IllegalStateException(e);
    }
}

简述其过程:创建一个新的AssetManager对象,通过反射调用它的addAssetPath方法,加载指定路径下的资源。然后遍历Activity列表,得到每个Activity的Resources对象,在通过反射得到Resources对象的mAsset字段(AssetManager类型),将mAsset字段设置为新创建的AssetManager对象。其后的代码逻辑,也采用类似的方式,根据SDK的版本不同,用不同的方式,得到Resources的弱引用集合,再遍历这个弱引用集合,将弱引用集合中的Resources的mAssets字段引用都替换成新创建的AssetManager对象。
可以看出,Instant Run的资源修复可以简单地总结为两个步骤:

  1. 创建新的AssetManager对象,通过反射调用addAssetPath方法加载外部资源,这样新创建的AssetManager就含有了新的外部资源。
  2. 将AssetManager类型的字段mAsset全部设置为新创建的AssetManager对象。

代码修复

代码修复主要有三种方案,分别是底层替换方法,类加载方案和Instant Run方案

类加载方案

类加载方案基于Dex分包方案,要了解Dex分包方案,需要先了解下65535限制和LinearAlloc限制

65535限制

随着应用程序的功能越来越复杂,代码量不断增大,引入的库越来越多,程序在编译时会提示65535限制:

com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536

这个异常说明程序中引用的方法数量不能超过65536个,产生这一问题的原因是系统的65536限制,这个限制归因于DVM bytecode的限制,DVM指令集的方法调用指令invoke-kind索引为16bits,最多能引用65535个方法。

LinearAlloc限制

在安装Apk时,如果遇到提示INSTALL_FAILED_DEXPORT,产生的原因就是LinearAlloc限制,DVM的LinearAlloc是一个固定的缓存区,当方法数超过了缓存区的大小时,就会报错。

为了解决65535限制和LinearAlloc限制,从而产生了Dex分包方案,Dex分包方案主要做的是在打包apk时,将程序代码分成多个dex文件,App启动时必须用到的类和这些类的直接引用类都放在主dex文件中,其他代码放在次dex文件中。当App启动时,先加载主dex文件,等到App启动后,再按需动态加载次dex文件。从而避免了主dex文件的65535限制和LinearAlloc限制。
Dex分包方案主要有两种:google官方方案和Dex自动拆包+动态加载方案。

在Android的ClassLoader加载过程中,有一个环节就是BaseDexClassLoader的findClass方法中,调用了DexPathList的findClass方法。

public Class findClass(String name, List suppressed) {
    for (Element element : dexElements) {
        Class clazz = element.findClass(name, definingContext, suppressed);
        if (clazz != null) {
            return clazz;
        }
    }


    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

在DexPathList的findClass方法中,遍历DexElements数组,调用Element的findClass方法,Element是DexPathList的静态内部类

static class Element {
    /**
     * A file denoting a zip file (in case of a resource jar or a dex jar), or a directory
     * (only when dexFile is null).
     */
    private final File path;
    private final DexFile dexFile;
    private ClassPathURLStreamHandler urlHandler;
    private boolean initialized;
    ...
    public Class findClass(String name, ClassLoader definingContext,
            List suppressed) {
        return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                : null;
    }
    ...
}

Element内部封装了DexFile,它用于加载dex文件,因此每一个dex文件对应着一个Element对象,多个Element对象组成了dexElements数组。当要查找某个类时,会先遍历dexElements数组,调用Element的findClass方法来查找。如果Element(dex文件)中找到该类就返回,否则就接着在下一个Element(dex文件)中去查找。根据这样的流程,我们先将有bug的类进行修改,再打包成包含dex文件的补丁包(如Patch.jar),将Patch.dex文件对应的Element对象放在dexElement数组的第一个元素,这样在DexPathList.findClass方法的执行时,会先从Patch.dex中找到已修复bug的目标类,根据ClassLoader的双亲委派模式,排在后面的dex文件中存在bug的类就不会加载了,从而实现了替换之前存在bug的类。这就是类加载方案的原理。
类加载方案

类加载方案需要重启App让ClassLoader重新加载新的类,为什么要重启App呢?这是因为类加载之后无法被卸载,之前存在bug的类一直在ClassLoader的缓存中,要想重新加载新的类替换存在bug的类,就需要重启App,让ClassLoader重新加载,因此采用类加载方案的热修复框架是不能即时生效的。虽然很多热修复框架都采用了类加载方案,但具体的实现细节和步骤还是有一些区别的。比如QQ空间的超级补丁和Nuwa,是按照上面说的将补丁包对应的Element元素放在dexElements数组的第一个元素使其优先加载。微信Tinker将新旧apk做了更改区分,得到了patch.dex,再将patch.dex与原来apk中的class.dex合并,生成新的class.dex,然后运行时,通过反射将class.dex放在dexElement数组的第一个元素。饿了么的Amigo则是将补丁包中的每一个dex文件对应的Element取出来,组成新的Element数组,在运行时通过反射用新的Element数组替换原来的dexElement数组。
采用类加载方案的主要以腾讯系为主,包括微信的Tinker、QQ空间的超级补丁、手机QQ的Qfix、饿了么的Amigo和Nuwa等

底层替换方案

底层替换方案不会再次加载新类,而是直接在Native层修改原有的类,由于在原有类上进行修改限制比较多,并且不能增减原有类的方法和字段。如果我们增加了方法数,那么方法索引数也会增加,这样访问方法时会无法通过找到正确的方法,同样的,字段也是类似的情况。底层替换方案和反射的原理有些关联,通过Method.invoke()方法来调用一个方法,其中invoke方法是一个native方法,对应jni层的Method_invoke函数,Method_invoke函数内部又会调用invokeMethod函数,并将java方法对应的javaMethod对象作为参数传进去,javaMethod在ART虚拟机中对应着一个ArtMethod指针,ArtMethod结构体中包含了java方法中的所有信息,包括方法入口、访问权限、所属的类和代码执行地址等。

class ArtMethod {
 …………
 protect:
  HeapReference declaring_class_;
  HeapReference> dex_cache_resolved_methods_;
  HeapReference> dex_cache_resolved_types_;
  uint32_t access_flags_;
  uint32_t dex_code_item_offset_;
  uint32_t dex_method_index_;
  uint32_t method_index_;
  struct PACKED(4) PtrSizedFields {
    void* entry_point_from_interpreter_;
    void* entry_point_from_jni_;
    void* entry_point_from_quick_compiled_code_;
 
#if defined(ART_USE_PORTABLE_COMPILER)
    void* entry_point_from_portable_compiled_code_;
#endif
  } ptr_sized_fields_;
  static GcRoot java_lang_reflect_ArtMethod_;
 
}
……

在ArtMethod结构体中,比较重要的字段是dex_cache_resolved_methods_和entry_point_from_quick_compiled_code_,他们是一个java方法的执行入口,当我们调用某个方法时,就会取得该方法的执行入口,通过执行入口就可以跳过去执行该方法,替换ArtMethod结构体中的字段或者替换整个ArtMethod结构体,就是底层替换方案的原理。AndFix采用的是替换ArtMethod结构体中的字段,这样会有兼容性问题,因为厂商可能会修改ArtMethod的结构体,导致替换失败。Sophix采用的是替换整个ArtMethod结构体,这样就不会有兼容性问题。底层替换方案直接替换了方法,可以立即生效,不需要重启App。采用底层替换方案的主要以阿里系为主,AndFix、Dexposed、阿里百川、Sophix。

Instant Run方案

除了资源修复,代码修复同样可以借鉴Instant Run的原理。Instant Run在第一次构建apk时,使用ASM在每个方法中注入一段代码。ASM是一个java字节码操控框架,它能够动态生成类或者增强现有类的功能。ASM可以直接生成class文件,也可以在类被加载到虚拟机之前,动态改变类的行为。

IncrementalChange localIncrementalChange = $change;//1
if (localIncrementalChange != null) {//2
      localIncrementalChange.access$dispatch(
              "onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
                      paramBundle });
      return;
  }

这里以某个Activity的onCreate方法为例,当我们执行Instant Run时,如果方法没有变化,$change则为null,不做任何处理,方法正常执行。如果方法有变化,就会生成替换类,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchLoaderImpl类,这个类的getPatchClasses方法会返回被修改的类的列表,根据这个列表,将上述代码中的$change设置为生成的替换类,因此满足了上述代码中的判断条件,会执行替换类的access$dispatch方法,在access$dispatch方法方法中会根据参数"onCreate.(Landroid/os/Bundle;)V",执行替换类的onCreate方法,从而实现了对onCreate方法的修改,借鉴Instant Run代码修复原理的热修复框架有Robust和Aceso。

动态链接库(.so)修复

热修复框架的动态链接库(.so)修复,主要是更新so文件,换句话说就是重新加载so文件。因此so修复的基本原理就是重新加载so文件。加载so文件主要用到了System类中的load方法和loadLibrary方法

@CallerSensitive
public static void load(String filename) {
    Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}

@CallerSensitive
public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}

其中load方法传入的参数是so文件在磁盘上的完整路径,用于加载指定路径下的so文件。loadLibrary方法的参数是so库的名称,用于加载apk安装后,从apk包中复制到/data/data/packagename/lib下的so文件,目前的so修复都是基于这两个方法。
System类的load方法和loadLibrary方法内部,各自调用了Runtime类load0方法和loadLibrary0方法

synchronized void load0(Class fromClass, String filename) {
        if (!(new File(filename).isAbsolute())) {
            throw new UnsatisfiedLinkError(
                "Expecting an absolute path of the library: " + filename);
        }
        if (filename == null) {
            throw new NullPointerException("filename == null");
        }
        String error = doLoad(filename, fromClass.getClassLoader());
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
    }

synchronized void loadLibrary0(ClassLoader loader, String libname) {
        if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        String libraryName = libname;
        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;
        }

        String filename = System.mapLibraryName(libraryName);
        List candidates = new ArrayList();
        String lastError = null;
        for (String directory : getLibPaths()) {
            String candidate = directory + filename;
            candidates.add(candidate);

            if (IoUtils.canOpenReadOnly(candidate)) {
                String error = doLoad(candidate, loader);
                if (error == null) {
                    return; // We successfully loaded the library. Job done.
                }
                lastError = error;
            }
        }

        if (lastError != null) {
            throw new UnsatisfiedLinkError(lastError);
        }
        throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
    }

从上述代码可以看出Runtime类的load0方法和loadLibrary0方法的执行,最终都会调用到doLoad方法。只是需要注意到,在loadLibrary0方法中,loader不为null时,doLoad方法的第一个参数filename,是通过loader.findLibrary方法获取的。findLibrary方法的实现在BaseDexClassLoader中。

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }
}

在BaseDexClassLoader的findLibrary方法内部调用了DexPathList的findLibrary方法。

public String findLibrary(String libraryName) {
    String fileName = System.mapLibraryName(libraryName);
    for (Element element : nativeLibraryPathElements) {
        String path = element.findNativeLibrary(fileName);
        if (path != null) {
            return path;
        }
    }
    return null;
}
final class DexPathList {
    static class Element {

        public String findNativeLibrary(String name) {
            maybeInit();
            if (isDirectory) {
                String path = new File(dir, name).getPath();
                if (IoUtils.canOpenReadOnly(path)) {
                    return path;
                }
            } else if (zipFile != null) {
                String entryName = new File(dir, name).getPath();
                if (isZipEntryExistsAndStored(zipFile, entryName)) {
                  return zip.getPath() + zipSeparator + entryName;
                }
            }
            return null;
        }
    }
}

mapLibraryName方法的功能是将xxx动态库的名字转换为libxxx.so,比如前面传递过来的nickname为sdk_jni, 经过该方法处理后返回的名字为libsdk_jni.so.
nativeLibraryPathElements表示所有的Native动态库, 包括app目录的native库和系统目录的native库。其中的每一个Element对应着一个so库文件。DexPathList的findLibrary方法就是用来通过so库的名称,从nativeLibraryPathElements中遍历每一个Element对象,findNativeLibrary方法去查找,如果存在就返回其路径。
so修复的其中一种方案就是就是将so的补丁文件对应的Element插入到nativeLibraryPathElements的第一个元素,这样就能使补丁文件的路径先被找到并返回。
接着看Runtime类的doLoad方法

private String doLoad(String name, ClassLoader loader) {
    String ldLibraryPath = null;
    String dexPath = null;
    if (loader == null) {
        ldLibraryPath = System.getProperty("java.library.path");
    } else if (loader instanceof BaseDexClassLoader) {
        BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
        ldLibraryPath = dexClassLoader.getLdLibraryPath();
    }
    synchronized (this) {
        return nativeLoad(name, loader, ldLibraryPath);
    }
}

Runtime类的doLoad方法最后会调用到调用到native层的nativeLoad函数,而nativeLoad函数又会调用到LoadNativeLibrary函数,so文件加载的主要逻辑就是在LoadNativeLibrary函数中。

bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, const std::string& path, jobject class_loader,
                                  std::string* error_msg) {
  error_msg->clear();

  SharedLibrary* library;
  Thread* self = Thread::Current();
  {
    MutexLock mu(self, *Locks::jni_libraries_lock_);
    library = libraries_->Get(path); //检查该动态库是否已加载
  }
  if (library != nullptr) {
    if (env->IsSameObject(library->GetClassLoader(), class_loader) == JNI_FALSE) {
      //不能加载同一个采用多个不同的ClassLoader
      return false;
    }
    ...
    return true;
  }

  const char* path_str = path.empty() ? nullptr : path.c_str();
  //通过dlopen打开动态共享库.该库不会立刻被卸载直到引用技术为空.
  void* handle = dlopen(path_str, RTLD_NOW);
  bool needs_native_bridge = false;
  if (handle == nullptr) {
    if (android::NativeBridgeIsSupported(path_str)) {
      handle = android::NativeBridgeLoadLibrary(path_str, RTLD_NOW);
      needs_native_bridge = true;
    }
  }

  if (handle == nullptr) {
    *error_msg = dlerror(); //打开失败
    VLOG(jni) << "dlopen(\"" << path << "\", RTLD_NOW) failed: " << *error_msg;
    return false;
  }


  bool created_library = false;
  {
    std::unique_ptr new_library(
        new SharedLibrary(env, self, path, handle, class_loader));
    MutexLock mu(self, *Locks::jni_libraries_lock_);
    library = libraries_->Get(path);
    if (library == nullptr) {
      library = new_library.release();
      //创建共享库,并添加到列表
      libraries_->Put(path, library);
      created_library = true;
    }
  }
  ...

  bool was_successful = false;
  void* sym;
  //查询JNI_OnLoad符号所对应的方法
  if (needs_native_bridge) {
    library->SetNeedsNativeBridge();
    sym = library->FindSymbolWithNativeBridge("JNI_OnLoad", nullptr);
  } else {
    sym = dlsym(handle, "JNI_OnLoad");
  }

  if (sym == nullptr) {
    was_successful = true;
  } else {
    //需要先覆盖当前ClassLoader.
    ScopedLocalRef old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
    self->SetClassLoaderOverride(class_loader);

    typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
    JNI_OnLoadFn jni_on_load = reinterpret_cast(sym);
    // 真正调用JNI_OnLoad()方法的过程
    int version = (*jni_on_load)(this, nullptr);

    if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
      fault_manager.EnsureArtActionInFrontOfSignalChain();
    }
    //执行完成后, 需要恢复到原来的ClassLoader
    self->SetClassLoaderOverride(old_class_loader.get());
    ...
  }

  library->SetResult(was_successful);
  return was_successful;
}

简述一下,LoadNativeLibrary函数主要做了三件事:

  1. 判断so文件是否已经被加载过,前后两次加载的ClassLoader是否为同一个,避免重复加载。
  2. 打开so文件并得到句柄,如果句柄获取失败,则返回false。创建新的SharedLibrary,如果传入的path对应的Library为空,就将新创建的SharedLibrary复制给Library,并将Library存储到libraries中。
  3. 查找JNI_OnLoad指针,根据不同情况设置was_successful的值,最终返回was_successful。

热修复框架的so修复主要有两个方案:

  • 将so补丁插入到NativeLibraryElement数组的前部,让so补丁的路径先被返回和加载。
  • 调用System类的load方法来接管so的加载入口。

本文参考:
《Android进阶解密》第十三章
源码简析之ArtMethod结构与涉及技术介绍
loadLibrary动态库加载过程分析

你可能感兴趣的:(Android热修复及原理总结和介绍)