跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Android 热修复原理与 HotFix 框架实现详解

深入解析了 Android 热修复技术,重点阐述了 QQ 空间 HotFix 方案的实现原理。文章首先介绍了 Android Dex 分包机制及 ClassLoader 的加载顺序,指出多 Dex 场景下的类冲突解决策略。随后分析了 CLASS_ISPREVERIFIED 标志带来的验证问题及其通过构造方法注入引用代码的解决方案。接着详细拆解了 HotFix 框架如何利用 Javassist 在构建阶段修改 Class 文件,并通过 Gradle 插件 Hook 执行时机。核心部分展示了如何通过反射动态修改 BaseDexClassLoader 内部的 dexElements 数组,将补丁 Dex 插入至加载路径首位,并针对不同 API 版本及厂商定制系统提供了具体的适配代码。最后总结了该方案的优缺点及适用场景。

XiaoPingzi发布于 2025/2/7更新于 2026/9/1270 浏览
Android 热修复原理与 HotFix 框架实现详解

Android 热修复原理与 HotFix 框架实现详解

在 Android 开发中,热修复技术允许在不重新发布应用的情况下修复线上 Bug。本文详细分析 QQ 空间热修复方案(HotFix)的核心原理及实现细节。

Android Dex 分包原理介绍

QQ 空间热修复方案基于 Android Dex 分包基础之上。简单来说,Android Dex 分包的原理是将多个 dex 文件塞入到 App 的 ClassLoader 之中。但是 Android Dex 拆包方案中的类是没有重复的,如果 classes.dex 和 classes1.dex 中有重复的类,当两者都具有同一个类的时候,ClassLoader 会选择加载哪个类呢?这要从 ClassLoader 的源码入手。

加载类是通过 ClassLoader 的 loadClass 方法实现的,我们看一下 loadClass 的源码:

/**
 * Loads the class with the specified name. Invoking this method is
 * equivalent to calling {@code loadClass(className, false)}.
 *
 * @return the {@code Class} object.
 * @param className
 *            the name of the class to look for.
 * @throws ClassNotFoundException
 *             if the class can not be found.
 */
public Class<?> loadClass(String className) throws ClassNotFoundException {
    return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    Class<?> clazz = findLoadedClass(className);

    if (clazz == null) {
        ClassNotFoundException suppressed = null;
        try {
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            suppressed = e;
        }

        if (clazz == null) {
            try {
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }

    return clazz;
}

ClassLoader 是基于双亲代理模型的。具体来说,ClassLoader 用 loadClass 方法调用了 findClass 方法。点进去发现 findClass 是抽象方法,而这个方法的实现是在它的子类 BaseDexClassLoader 中。BaseDexClassLoader 重载了这个方法。

进入 BaseDexClassLoader 类的 findClass 方法中:

// BaseDexClassLoader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class clazz = pathList.findClass(name);

    if (clazz == null) {
        throw new ClassNotFoundException(name);
    }

    return clazz;
}

接着查看 DexPathList 的实现:

// DexPathList
public Class findClass(String name) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;

        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext);
            if (clazz != null) {
                return clazz;
            }
        }
    }

    return null;
}

最后查看 DexFile 的实现:

// DexFile
public Class loadClassBinaryName(String name, ClassLoader loader) {
    return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);

一个 ClassLoader 可以包含多个 dex 文件,每个 dex 文件是一个 Element,多个 dex 文件排列成一个有序的数组 dexElements。当找类的时候,会按顺序遍历 dex 文件,然后从当前遍历的 dex 文件中找类,如果找到则返回,如果找不到从下一个 dex 文件继续查找。理论上,如果在不同的 dex 中有相同的类存在,那么会优先选择排在前面的 dex 文件的类。

所以,QQ 空间正是基于 ClassLoader 的这个原理,把有问题的类打包到一个 dex(patch.dex)中去,然后把这个 dex 插入到 Elements 的最前面。

关于如何进行 dex 分包后面再单独进行分析。

CLASS_ISPREVERIFIED 的问题

采用 dex 分包方案会遇到的问题,也就是 CLASS_ISPREVERIFIED 的问题。简单来概括就是:在虚拟机启动的时候,当 verify 选项被打开的时候,如果 static 方法、private 方法、构造函数等,其中的直接引用(第一层关系)到的类都在同一个 dex 文件中,那么该类就会被打上 CLASS_ISPREVERIFIED 标志。

那么,我们要做的就是,阻止该类打上 CLASS_ISPREVERIFIED 的标志。注意下,是阻止引用者的类。也就是说,假设你的 app 里面有个类叫做 AClass,在其内部引用了 BClass。发布过程中发现 BClass 有编写错误,那么想要发布一个新的 BClass 类,你就要阻止 AClass 这个类打上 CLASS_ISPREVERIFIED 的标志。也就是说,你在生成 apk 之前,就需要阻止相关类打上 CLASS_ISPREVERIFIED 的标志了。

如何阻止?简单来说,让 AClass 在构造方法中,去引用别的 dex 文件,比如 C.dex 中的某个类即可。所以总结下来,防止这个错误,只需要:

  1. 动态改变 BaseDexClassLoader 对象间接引用的 dexElements;
  2. 在 app 打包的时候,阻止相关类去打上 CLASS_ISPREVERIFIED 标志。

热修复框架 HotFix 解析

采用 QQ 空间的热修复方案而实现的开源热修复框架就是 HotFix。说到了使用 dex 分包方案会遇到 CLASS_ISPREVERIFIED 问题,而解决方案就是在 dx 工具执行之前,将所有的 class 文件进行修改,在其构造中添加 System.out.println(dodola.hackdex.AntilazyLoad.class),然后继续打包的流程。注意:AntilazyLoad.class 这个类是独立在 hack.dex 中。

Dex 分包方案实现需要关注以下问题

  1. 如何解决 CLASS_ISPREVERIFIED 问题
  2. 如何将修复的.dex 文件插入到 dexElements 的最前面

解决 CLASS_ISPREVERIFIED 问题

那么如何达到这个目的呢?在 HotFix 中采用的 javassist 来达到这个目的,以下是 HotFix 中的 PatchClass.groovy 代码:

public class PatchClass {
    /**
     * 植入代码
     * @param buildDir 是项目的 build class 目录,就是我们需要注入的 class 所在地
     * @param lib 这个是 hackdex 的目录,就是 AntilazyLoad 类的 class 文件所在地
     */
    public static void process(String buildDir, String lib) {

        println(lib)
        ClassPool classes = ClassPool.getDefault()
        classes.appendClassPath(buildDir)
        classes.appendClassPath(lib)

        //下面的操作比较容易理解,在将需要关联的类的构造方法中插入引用代码
        CtClass c = classes.getCtClass("dodola.hotfix.BugClass")
        if (c.isFrozen()) {
            c.defrost()
        }
        println("====添加构造方法====")
        def constructor = c.getConstructors()[0];
        constructor.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")
        c.writeFile(buildDir)


        CtClass c1 = classes.getCtClass("dodola.hotfix.LoadBugClass")
        if (c1.isFrozen()) {
            c1.defrost()
        }
        println("====添加构造方法====")
        def constructor1 = c1.getConstructors()[0];
        constructor1.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")
        c1.writeFile(buildDir)


    }
    
    static void growl(String title, String message) {
        def proc = ["osascript", "-e", "display notification \"${message}\" with title \"${title}\""].execute()
        if (proc.waitFor() != 0) {
            println "[WARNING] ${proc.err.text.trim()}"
        }
    }
}

其实内部做的逻辑就是:通过 ClassPool 对象,然后添加 classpath。然后从 classpath 中找到 LoadBugClass,拿到其构造方法,在其中插入一行代码。

到这里插入代码的操作已经完成,但是还存在另外一个问题,那就是如何在 dx 之前去进行上述脚本的操作?答案就在 HotFix 的 app/build.gradle 中。

apply plugin: 'com.android.application'

task('processWithJavassist') << {
    String classPath = file('build/intermediates/classes/debug')//项目编译 class 所在目录
    dodola.patch.PatchClass.process(classPath, project(':hackdex').buildDir
            .absolutePath + '/intermediates/classes/debug')//第二个参数是 hackdex 的 class 所在目录

}
buildTypes {
    debug {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}
applicationVariants.all { variant ->
    variant.dex.dependsOn << processWithJavassist //在执行 dx 命令之前将代码打入到 class 中
}

可以看到在 build.gradle 中,在执行 dx 之前,会先执行 processWithJavassist 这个任务。这样会执行 PatchClass.groovy 的脚本,在构造方法中进行注入。

将修复的.dex 文件插入 dexElements

寻找 class 是遍历 dexElements;然后我们的 AntilazyLoad.class 实际上并不包含在 apk 的 classes.dex 中,并且根据上面描述的需要,我们需要将 AntilazyLoad.class 这个类打成独立的 hack_dex.jar,注意不是普通的 jar,必须经过 dx 工具进行转化。

具体做法:

jar cvf hack.jar dodola/hackdex/*
dx  --dex --output hack_dex.jar hack.jar 

还记得之前我们将所有的类的构造方法中都引用了 AntilazyLoad.class,所以我们需要把 hack_dex.jar 插入到 dexElements,而在 hotfix 中,就是在 Application 中完成这个操作的。

public class HotfixApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
        Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
        try {
            this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

在 app 的私有目录创建一个文件,然后调用 Utils.prepareDex 将 assets 中的 hackdex_dex.jar 写入该文件。Utils.prepareDex 中其实就是文件的读写操作,前提是你把 hackdex_dex.jar 放入到 assets 中。

public class Utils {
    private static final int BUF_SIZE = 2048;

    public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) {
        BufferedInputStream bis = null;
        OutputStream dexWriter = null;

        try {
            bis = new BufferedInputStream(context.getAssets().open(dex_file));
            dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath));
            byte[] buf = new byte[BUF_SIZE];
            int len;
            while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
                dexWriter.write(buf, 0, len);
            }
            dexWriter.close();
            bis.close();
            return true;
        } catch (IOException e) {
            if (dexWriter != null) {
                try {
                    dexWriter.close();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            }
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            }
            return false;
        }
    }
}

接下来 HotFix.patch 就是去反射去修改 dexElements 了。

public static void patch(Context context, String patchDexFile, String patchClassName) {
    if (patchDexFile != null && new File(patchDexFile).exists()) {
        try {
            if (hasLexClassLoader()) {
                injectInAliyunOs(context, patchDexFile, patchClassName);
            } else if (hasDexClassLoader()) {
                injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
            } else {
                injectBelowApiLevel14(context, patchDexFile, patchClassName);
            }
        } catch (Throwable th) {
        }
    }
}

可以看到 patch 方法中有几个分支,说白了是根据不同的系统中 ClassLoader 的类型来做相应的处理。

阿里云 OS 适配
private static void injectInAliyunOs(Context context, String patchDexFile, String patchClassName)
    throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException,
    InstantiationException, NoSuchFieldException {
    PathClassLoader obj = (PathClassLoader) context.getClassLoader();
    String replaceAll = new File(patchDexFile).getName().replaceAll("\\.[a-zA-Z0-9]+", ".lex");
    Class cls = Class.forName("dalvik.system.LexClassLoader");
    Object newInstance =
        cls.getConstructor(new Class[] {String.class, String.class, String.class, ClassLoader.class}).newInstance(
            new Object[] {context.getDir("dex", 0).getAbsolutePath() + File.separator + replaceAll,
                context.getDir("dex", 0).getAbsolutePath(), patchDexFile, obj});
    cls.getMethod("loadClass", new Class[] {String.class}).invoke(newInstance, new Object[] {patchClassName});
    setField(obj, PathClassLoader.class, "mPaths",
        appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(newInstance, cls, "mRawDexPath")));
    setField(obj, PathClassLoader.class, "mFiles",
        combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(newInstance, cls, "mFiles")));
    setField(obj, PathClassLoader.class, "mZips",
        combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(newInstance, cls, "mZips")));
    setField(obj, PathClassLoader.class, "mLexs",
        combineArray(getField(obj, PathClassLoader.class, "mLexs"), getField(newInstance, cls, "mDexs")));
}

上述方法中的 LexClassLoader 应该是阿里自己的 ClassLoader,可以看到上面将修复的文件的结尾都换成了.lex 的结尾,这些文件就是专门需要通过 LexClassLoader 进行加载的。

API 14 以下适配

我们分 API 14 以上和以下进行分析。

API 14 以下

private static void injectBelowApiLevel14(Context context, String str, String str2)
    throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
    PathClassLoader obj = (PathClassLoader) context.getClassLoader();
    DexClassLoader dexClassLoader =
        new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader());
    dexClassLoader.loadClass(str2);
    setField(obj, PathClassLoader.class, "mPaths",
        appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class,
                "mRawDexPath")
        ));
    setField(obj, PathClassLoader.class, "mFiles",
        combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class,
                "mFiles")
        ));
    setField(obj, PathClassLoader.class, "mZips",
        combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class,
            "mZips")));
    setField(obj, PathClassLoader.class, "mDexs",
        combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class,
            "mDexs")));
    obj.loadClass(str2);
}

通过 setField 方法将 mPaths 属性,修改为通过 appendArray 方法创造的新元素。

private static Object getField(Object obj, Class cls, String str)
    throws NoSuchFieldException, IllegalAccessException {
    Field declaredField = cls.getDeclaredField(str);
    declaredField.setAccessible(true);
    return declaredField.get(obj);
}
private static Object appendArray(Object obj, Object obj2) {
    Class componentType = obj.getClass().getComponentType();
    int length = Array.getLength(obj);
    Object newInstance = Array.newInstance(componentType, length + 1);
    Array.set(newInstance, 0, obj2);
    for (int i = 1; i < length + 1; i++) {
        Array.set(newInstance, i, Array.get(obj, i - 1));
    }
    return newInstance;
}

而 appendArray 中就是创建一个新的 Array,把 obj2 插入到 obj 的前面,注意这里的 obj2 长度只有 1。

所以,在 injectBelowApiLevel14 的以下方法中,就是把 mRawDexPath 的元素插入到 mPaths 中所有元素之前,而重新组合而成的新 mPaths 替换掉旧的 mPaths。

setField(obj, PathClassLoader.class, "mPaths",
    appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class,
            "mRawDexPath")
    ));

接下来的替换,是通过 combineArray 生成的新元素替换掉旧元素,这里分别是 mFiles,mZips,mDexs。

setField(obj, PathClassLoader.class, "mFiles",
    combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class,
            "mFiles")
    ));
setField(obj, PathClassLoader.class, "mZips",
    combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class,
        "mZips")));
setField(obj, PathClassLoader.class, "mDexs",
    combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class,
        "mDexs")));

于是我们需要看一下 combineArray 方法里面做了什么。

private static Object combineArray(Object obj, Object obj2) {
    Class componentType = obj2.getClass().getComponentType();
    int length = Array.getLength(obj2);
    int length2 = Array.getLength(obj) + length;
    Object newInstance = Array.newInstance(componentType, length2);
    for (int i = 0; i < length2; i++) {
        if (i < length) {
            Array.set(newInstance, i, Array.get(obj2, i));
        } else {
            Array.set(newInstance, i, Array.get(obj, i - length));
        }
    }
    return newInstance;
}

逻辑也很简单,也就是两个数组的合并而已。

API 14 以上适配

API 14 以上

private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
    throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
    PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
    Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
        getDexElements(getPathList(
            new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
    Object a2 = getPathList(pathClassLoader);
    setField(a2, a2.getClass(), "dexElements", a);
    pathClassLoader.loadClass(str2);
}

根据 context 拿到 PathClassLoader,然后通过 getPathList(pathClassLoader),拿到 PathClassLoader 中的 pathList 对象,在调用 getDexElements 通过 pathList 取到 dexElements 对象。

private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
    return getField(obj, obj.getClass(), "dexElements");
}
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
    IllegalAccessException {
    return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}

同样是通过 combineArray 方法,对数组进行合并,合并完成后,将新的数组通过反射的方式设置给 pathList。

通过上面的一系列流程,那么 hack_dex.jar 已经插入到 dexElements 最前面了,补丁插入的过程也和 hack_dex.jar 的插入流程是一致的。

总结与展望

至此,dex 分包方案实现热修复的 HotFix 的分析就已经完毕了。HotFix 方案的核心在于利用 ClassLoader 的双亲委派机制漏洞,通过反射修改 DexPathList 中的 dexElements 数组,将补丁 dex 置于加载列表首位,从而实现类覆盖。

然而,这种方案也存在一定的局限性:

  1. 兼容性:不同厂商 ROM 可能修改了 ClassLoader 的内部字段名或结构,导致反射失效。
  2. 性能开销:频繁的反射操作和数组复制会在一定程度上影响应用启动速度和运行性能。
  3. Native 库支持:对于涉及 Native 层的 Bug,单纯修改 Java 层无法生效,需要配合其他方案。
  4. 安全性:热修复机制本身可能被恶意利用,需做好签名校验和完整性检查。

在实际生产环境中,建议结合 Tinker、Sophix 等更成熟的商业或开源方案,它们通常针对上述问题做了更好的兼容性和稳定性优化。开发者在选择热修复方案时,应充分评估业务需求、目标机型覆盖率以及维护成本。

目录

  1. Android 热修复原理与 HotFix 框架实现详解
  2. Android Dex 分包原理介绍
  3. CLASS_ISPREVERIFIED 的问题
  4. 热修复框架 HotFix 解析
  5. Dex 分包方案实现需要关注以下问题
  6. 解决 CLASS_ISPREVERIFIED 问题
  7. 将修复的.dex 文件插入 dexElements
  8. 阿里云 OS 适配
  9. API 14 以下适配
  10. API 14 以上适配
  11. 总结与展望

更多推荐文章

查看全部
  • 大语言模型原理必读的 10 篇核心论文
  • Python 数据可视化:9 种常用图表及实现方法
  • Hibernate 集合映射实战:Set、List、Bag 与 Map 配置详解
  • AI Skills:前端新的效率神器
  • OpenClaw v2026.3.7 版本功能详解:AI 代理框架更新
  • RTX 3060 12G 量化部署 CodeLlama-7B 实战指南
  • 机器人避障与导航系统开发理论与实践
  • 三本关键 AI 大模型开发书籍推荐与学习指南
  • VSCode Copilot 聊天加载超时问题修复指南
  • AI 技能(Skills):面向任务自动化的模块化执行范式
  • 时序数据库架构与生态重构:Apache IoTDB 价值解析
  • LeRobot 深度解析:5 大核心模块构建机器人学习系统
  • 向日葵 MCP 接入 AI:实现跨设备远程控制与自动化操作
  • .NET 集成 GoView 低代码可视化大屏实战指南
  • VMware 虚拟机安装 CentOS 并部署 OpenClaw
  • AG-UI:连接 AI 智能体与用户应用的开放协议
  • Minecraft 开源注入式客户端 LiquidBounce 使用指南
  • 2026 年 3 月全球 AI 前沿动态:模型、智能体与硬件突破
  • Semantic Kernel Python 进阶:Prompt 模板函数嵌套调用实战
  • Python+AI 学习路线指南:从入门到实战

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online