Android APP 热修复原理

版权归作者所有,如有转发,请注明文章出处:cyrus-studio.github.io/blog/

dexElements

Android 的 ClassLoader(如 PathClassLoader、DexClassLoader)内部结构如下:

yaml 复制代码
BaseDexClassLoader
 └── pathList : DexPathList
       └── dexElements : Array<DexPathList.Element>

DexPathList 是 BaseDexClassLoader 用来管理 .dex 文件、.apk 和 .jar 的内部类,负责构建和维护类加载搜索路径。

aospxref.com/android-10....

dexElements 是 DexPathList 中用于保存所有 .dex 文件对应的元素数组,每个 Element 表示一个可用于加载类和资源的路径项,类加载时会依次查找这些元素。

aospxref.com/android-10....

findClass 方法迭代 dexElements 数组查找类

aospxref.com/android-10....

热修复原理

合并多个 dexElements 数组是插件化框架或热修复系统中的常见操作。你可以通过反射将多个 dex 元素合并成一个新的数组,然后注入回去。

这就是热修复的原理,把 新的 dex 添加到 dexElements 前面,那么 findClass 的时候就会优先使用最新的 dex 中的类

代码实现

  1. 通过反射拿到 ClassLoader 中的 dexElements 数组
kotlin 复制代码
fun getDexElementsFrom(classLoader: ClassLoader): Array<Any>? {
    return try {
        // 1. 拿到 pathList 字段
        val baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader")
        val pathListField = baseDexClassLoaderClass.getDeclaredField("pathList")
        pathListField.isAccessible = true
        val pathList = pathListField.get(classLoader)

        // 2. 拿到 dexElements 字段
        val pathListClass = pathList.javaClass
        val dexElementsField = pathListClass.getDeclaredField("dexElements")
        dexElementsField.isAccessible = true
        @Suppress("UNCHECKED_CAST")
        dexElementsField.get(pathList) as? Array<Any>
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}
  1. 合并两个 ClassLoader 中的 dexElements 数组
kotlin 复制代码
fun mergeDexElements(first: Array<Any>, second: Array<Any>): Array<Any> {
    val elementClass = first.javaClass.componentType ?: throw IllegalArgumentException("first is not an array")

    val totalLength = first.size + second.size
    val result = java.lang.reflect.Array.newInstance(elementClass, totalLength) as Array<Any>

    // 拷贝数组
    System.arraycopy(first, 0, result, 0, first.size)
    System.arraycopy(second, 0, result, first.size, second.size)

    return result
}
  1. 替换掉 PathClassLoader 中的 dexElements
kotlin 复制代码
@SuppressLint("DiscouragedPrivateApi")
fun injectDexElementsToClassLoader(classLoader: ClassLoader, newDexElements: Array<Any>) {
    try {
        val pathListField = Class.forName("dalvik.system.BaseDexClassLoader")
            .getDeclaredField("pathList").apply { isAccessible = true }
        val pathList = pathListField.get(classLoader)

        val dexElementsField = pathList.javaClass
            .getDeclaredField("dexElements").apply { isAccessible = true }
        dexElementsField.set(pathList, newDexElements)

        Log.d(TAG, "✅ dexElements successfully replaced!")
    } catch (e: Exception) {
        e.printStackTrace()
        Log.d(TAG,"❌ Failed to inject dexElements")
    }
}

示例代码

创建一个 Activity ,有两个按钮:

  • 热修复:加载 sdcard 上的 apk 文件,执行热修复逻辑

  • PluginClass.getString:通过 Class.forName 加载 com.cyrus.example.plugin.PluginClass 类 创建对象并调用 getString方法,并显示结果

1. plugin

创建 plugin 工程主要包含这两个类:

PluginClass 类源码如下:

kotlin 复制代码
package com.cyrus.example.plugin

class PluginClass {

    fun getString(): String {
        return "String from plugin."
    }

}

编译 apk

把 apk 推送到设备 sdcard

bash 复制代码
adb push plugin-debug.apk /sdcard/Android/data/com.cyrus.example/files

2. app

app 工程也有一个 PluginClass,但是 getString 方法返回结果不一样。

kotlin 复制代码
package com.cyrus.example.plugin

class PluginClass {

    fun getString(): String {
        return "String from app."
    }

}

热修复代码:

scss 复制代码
val pathClassLoader = classLoader

// 1. 创建自定义 ClassLoader 实例,加载 sdcard 上的 apk
val classLoader = DexClassLoader(
    apkPath,
    null,
    this.packageResourcePath,
    pathClassLoader.parent
)

// 2. 通过反射拿到 ClassLoader 中的 dexElements 数组
val baseDexElements = getDexElementsFrom(pathClassLoader)
val pluginDexElements = getDexElementsFrom(classLoader)

// 3. 合并两个 ClassLoader 中的 dexElements 数组
val merged = mergeDexElements(pluginDexElements!!, baseDexElements!!)

// 4. 替换掉 PathClassLoader 中的 dexElements
injectDexElementsToClassLoader(pathClassLoader, merged)

通过 Class.forName 加载 com.cyrus.example.plugin.PluginClass 类 创建对象并调用 getString方法,并显示结果

kotlin 复制代码
try {
    val clazz = Class.forName("com.cyrus.example.plugin.PluginClass")
    val obj = clazz.getDeclaredConstructor().newInstance()
    val method: Method = clazz.getDeclaredMethod("getString")
    method.invoke(obj) as String
} catch (e: Exception) {
    "调用失败: ${e.message}"
}

点击 PluginClass.getString 按钮结果显示 String from app.

点击热修复按钮

再点击 PluginClass.getString 按钮结果显示 String from plugin.

Frida 打印 ClassLoader

通过 Frida 打印热修复后的 app 中所有 ClassLoader。

相关文章:使用 Frida Hook Android App

classloader_utils.js

javascript 复制代码
function printAllClassLoaders() {
    Java.perform(() => {
        Java.enumerateClassLoaders({
            onMatch: function (loader) {
                const desc = loader.toString();
                const parent = loader.getParent();
                console.log('ClassLoader:', desc);
                console.log('  ↳ Parent:', parent);
            },
            onComplete: function () {
                console.log('=== Finished enumerating ClassLoaders ===');
            }
        });
    });
}

执行脚本

r 复制代码
frida -H 127.0.0.1:1234 -F -l classloader_utils.js

枚举所有存在的 ClassLoader 实例

scss 复制代码
printAllClassLoaders()

日志输出如下:

javascript 复制代码
[Remote::AndroidExample]-> printAllClassLoaders()
ClassLoader: dalvik.system.PathClassLoader[DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system/product/lib64, /system/lib64, /system/product/lib64]]]
  ↳ Parent: java.lang.BootClassLoader@96b2bbd
ClassLoader: java.lang.BootClassLoader@96b2bbd
  ↳ Parent: null
ClassLoader: dalvik.system.PathClassLoader[DexPathList[[zip file "/sdcard/Android/data/com.cyrus.example/files/plugin-debug.apk", zip file "/data/ap
p/com.cyrus.example-aFNKPWnZMdeRCd6UR_DPaA==/base.apk"],nativeLibraryDirectories=[/data/app/com.cyrus.example-aFNKPWnZMdeRCd6UR_DPaA==/lib/arm64, /data/app/com.cyrus.example-aFNKPWnZMdeRCd6UR_DPaA==/base.apk!/lib/arm64-v8a, /system/lib64, /system/product/lib64]]]
  ↳ Parent: java.lang.BootClassLoader@96b2bbd
=== Finished enumerating ClassLoaders ===

从日志可以看到 PathClassLoader 的 DexPathList 中 目前有两个 Element:plugin-debug.apk 和 base.apk,而且 plugin-debug.apk 前面所以会优先找到 plugin 中的 PluginClass

完整源码

开源地址:github.com/CYRUS-STUDI...

相关文章:

相关推荐
消失的旧时光-19436 分钟前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon1 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon1 小时前
VSYNC 信号完整流程2
android
dalancon1 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013842 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android3 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才3 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶4 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle
汪海游龙4 小时前
开源项目 Trending AI 招募 Google Play 内测人员(12 名)
android·github
qq_283720055 小时前
MySQL技巧(四): EXPLAIN 关键参数详细解释
android·adb