Android热修复框架及原理

902 阅读3分钟

Android 热修复技术概述

图片 2.png

1. Tinker

Tinker 是腾讯推出的一款热修复框架,主要通过以下几个方面来实现修复:

  • 类差异:采用腾讯自研的 DexDiff 算法,针对 dex 格式开发,优化合成效率和差异包大小(类似 bsdiff,可以忽略文件格式,针对二进制数据)。
  • so 替换:使用 bsdiff 自动计算 BaseApk 与修改后的 apk 差异,生成 patch.dex;运行后,通过 classloader 加载 patch.dex 与 apk 本身的 classes.dex 合成。

2. meituan/Robust

Robust 是美团推出的热修复框架,利用 install-run 原理,在代码编译期间插入代码(ChangeQuickRedirect)。如果这个不为 null,就会先执行插入的代码,通过插桩修改 class 文件,然后通过代理的方式运行修复后的代码。

3. Qzone

Qzone 热修复基于 multidex,把有 bug 的类单独打包进 dex,在运行时加载 dex 补丁。

4. alibaba/AndFix

AndFix 是阿里巴巴推出的热修复框架,通过 native 动态修改 ARTMethod 指向方法,替换 java 层方法。通过 native 层 hook java 层的代码,可以即时生效,但需要使用 NDK。

热修复机制

热修复的核心是通过修改 classloader 加载机制,把修复后的 dex 文件优先加载。具体步骤如下:

  1. 获取当前应用 PathClassLoader
  2. 反射获取 DexPathList 属性的 pathList
  3. 反射修改 pathList 的 dexElements
    1. 把补丁包 patch.dex 转化为 Element[],通过 makePathElements 进行转换(path)。
    2. 获取 pathList 的 dexElements 属性。
    3. 把 patch.dex 加入到原本 dexElements 的第一位,再赋值给 dexElements。

ClassLoader 加载方式

图片 1.png

ClassLoader 内部有数组【Element】,按序加载,加载到所需要的 class 就不会往后执行,热修复就是把补丁 dex 文件加载到数组的前面,保证优先加载。

DexClassLoader 和 PathClassLoader 的区别

  • DexClassLoader 多一个 odex 文件生成地址(optimizedDirectory)。
  • PathClassLoader 地址固定生成到 /data/data/包名 下。

Android 8.0 开始,DexClassLoader 的 optimizedDirectory 参数被弃用,都会使用固定地址。

  • Android framework 的 dex 文件由 BootClassLoader 加载。
  • Android 应用层类加载器 由 PathClassLoader 加载。
  • 额外的 dex 文件 由 DexClassLoader 加载。

双亲委托机制(类似装饰器模式)

优点:
  1. 安全:防止核心 API 被修改(核心库还是由 BootClassLoader 加载)。
  2. 避免重复加载:当父类加载过,就不用重复加载。

classloader 加载机制,当调用 loadClass 方法会先查找内存中是否有加载缓存,没有的话,使用 parent.loadclass 加载,当都没有加载到的时候,自己本身才会加载。

加载过程:
  • ClassLoader.loadClass
 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 查看是否加载过这个class
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //双亲委托 先通过parentclassloader加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
              
                }

                if (c == null) {
                 
                    long t1 = System.nanoTime();
                    //没有的话,自身加载
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
  • BaseDexClassLoader.findClass
    this.pathList = new DexPathList(this, librarySearchPath);
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // ....
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
		//实际调用的是pathList.findClass
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c != null) {
            return c;
        }
        //...
        return c;
    }

  • DexPathList.findClass
    • DexPathList 内部有 Element[] dexElements,findClass 就是遍历这个数组,Element.DexFile(DexFile 是由 native 层加载 dex 文件生成的类,包含 dex 文件相关信息)就能获取到对应的 dex 文件,再由 DexFile.loadClassBinaryName 查找对应类。
    //DexPathList构造
    DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
      	//...
        this.definingContext = definingContext;

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // 通过makeDexElements加载dex文件为Elements[]shuju 
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, 		isTrusted);

      	//...
    }
	//存储dexfile
	private Element[] dexElements;

	public Class<?> findClass(String name, List<Throwable> suppressed) {
		//便利Element取出DexFile
        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;
    }
  • Element.findClass
public Class<?> findClass(String name, ClassLoader definingContext,List<Throwable>suppressed) {
    return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed): null;
}

热修复就是把自己的 dex 加入到 DexFile.Element 数组中,并保证加载在 olddex 前面。