Android进阶(八)热修复基本原理

一、代码修复

1、类加载方案

(1)Dex分包原理

单个Dex文件里面方法数不能超过65536个方法。

(1)原因:
因为android会把每一个类的方法id检索起来,存在一个链表结构里面。但是这个链表的长度是用一个short类型来保存的, short占两个字节(保存-2的15次方到2的15次方-1,即-32768~32767),最大保存的数量就是65536。

(2)解决方案:

  • 精简方法数量,删除没用到的类、方法、第三方库。
  • 使用ProGuard去掉一些未使用的代码
  • 对部分模块采用本地插件化的方式。
  • 分割Dex

Dex分包方案主要做的是在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其他代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动后再动态的加载次Dex。

(2)类加载修复方案

如果Key.Class文件中存在异常,将该Class文件修复后,将其打入Patch.dex的补丁包
(1) 方案一:
通过反射获取到PathClassLoader中的DexPathList,然后再拿到 DexPathList中的Element数组,将Patch.dex放在Element数组dexElements的第一个元素,最后将数组进行合并后并重新设置回去。在进行类加载的时候,由于ClassLoader的双亲委托机制,该类只被加载一次,也就是说Patch.dex中的Key.Class会被加载。

(2)方案二:
提供dex差量包patch.dex,将patch.dex与应用的classes.dex合并成一个完整的dex,完整dex加载后得到dexFile对象,作为参数构建一个Element对象,然后整体替换掉旧的dex-Elements数组。(Tinker)

(3)类加载方案的限制

方案一:

  • 由于类是无法进行卸载,所以类如果需要重新加载,则需要重启App,所以类加载修复方案不是即时生效的。
  • 在ART模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到patch.dex中,导致补丁包大,耗时严重。

方案二:

  • 下次启动修复
  • dex合并内存消耗可能导致OOM,最终dex合并失败

2、底层替换方案

(1)基本方案

主要是在Native层替换原有方法,ArtMethod结构体中包含了Java方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等。替换ArtMethod结构体中的字段或者替换整个ArtMethod结构体,就是底层替换方案。由于直接替换了方法,可以立即生效不需要重启。

(2)优缺点

(1)缺点

  • 不能够增减原有类的方法和字段,如果我们增加了方法数,那么方法索引数也会增加,这样访问方法时会无法通过索引找到正确的方法。
  • 平台兼容性问题,如果厂商对ArtMethod结构体进行了修改,替换机制就有问题。

(2)优点

  • Bug修复的即时性
  • 生成的PATCH体积小,性能影响低

二、资源修复

1、Instant Run

核心代码:runtime/MonkeyPatcher.java

#MonkeyPatcher
public static void monkeyPatchExistingResources(@Nullable Context context,
                                                @Nullable String externalResourceFile,
                                                @Nullable Collection<Activity> activities) {
    ......                                        
    try {
        // Create a new AssetManager instance and point it to the resources installed under
        // (1)通过反射创建了一个newAssetManager,调用addAssetPath添加了sdcard上的资源包
        AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
        Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
        mAddAssetPath.setAccessible(true);
        if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }
        // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
        // in L, so we do it unconditionally.
        Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
        mEnsureStringBlocks.setAccessible(true);
        mEnsureStringBlocks.invoke(newAssetManager);
        if (activities != null) {
            //(2)反射获取Activity中AssetManager的引用,替换成新创建的newAssetManager
            for (Activity activity : activities) {
                Resources resources = activity.getResources();
                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.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);
                    }
                ......
        }
        //(3)遍历Resource弱引用的集合,将AssetManager替换成newAssetManager
        for (WeakReference<Resources> wr : references) {
            Resources resources = wr.get();
            if (resources != null) {
                // Set the AssetManager of the Resources instance to our brand new one
                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,并反射调用addAssertPath加载sdcard中的新资源包,这样就得到一个含有所有新资源的AssetManager
  • 将原来引用到AssetManager的地方,通过反射把引用处替换为新的AssetManager

2、资源包替换(Sophix)

默认由Android SDK编译出来的apk,其资源包的package id为0x7f。framework-res.jar的资源package id为0x01

  • 构造一个package id为0x66的资源包(非0x7f和0x01),只包含已经改变的资源项。
  • 由于不与已经加载的Ox7f冲突,所以可以通过原有的AssetManager的addAssetPath加载这个包。

三、SO库修复

本质是对native方法的修复和替换

1、so库加载

(1)通过以下方法加载so库

#System
public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
参数为so库名称,位于apk的lib目录下

public static void load(String filename) {
    Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
}
加载外部自定义so库文件,参数为so库在磁盘中的完整路径
复制代码
private static native String nativeLoad(String filename, ClassLoader loader, String librarySearchPath);
复制代码

最终都是调用了native方法nativeLoad,参数fileName为so在磁盘中的完整路径名

(2)遍历nativeLibraryDirectories目录

#DexPathList
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;
}
复制代码

类似于类加载的findClass方法,在数组中每一个元素对应一个so库,最终返回了so的路径。如果将so补丁添加到数组的最前面,在调用方法加载so库时,会先将补丁so的路径返回。

2、SO修复方案

(1)接口替换

提供方法替代System.loadLibrary方法

  • 如果存在补丁so,则加载补丁so库,不去加载apk安装目录下的so库
  • 如果不存在补丁so,调用System.loadLibrary去加载安装apk目录下的so库

(2)反射注入

因为加载so库会遍历nativeLibraryDirectories

  • 通过反射将补丁so库的路径插入到nativeLibraryDirectories数组的最前面
  • 遍历nativeLibraryDirectories时,就会将补丁so库进行返回并加载,从而达到修复目的

四、热修复框架分析

  • 底层替换方案:阿里的AndFix、HotFix
  • 类加载方案:QQ空间补丁技术、微信的Tinker方案、饿了么的Amigo
  • 二者结合:Sophix

参考资料:

猜你喜欢

转载自juejin.im/post/5c66bb866fb9a049eb3c76fc