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...

相关文章:

相关推荐
maki0774 小时前
虚幻版Pico大空间VR入门教程 05 —— 原点坐标和项目优化技巧整理
android·游戏引擎·vr·虚幻·pico·htc vive·大空间
千里马学框架4 小时前
音频焦点学习之AudioFocusRequest.Builder类剖析
android·面试·智能手机·车载系统·音视频·安卓framework开发·audio
fundroid7 小时前
掌握 Compose 性能优化三步法
android·android jetpack
TeleostNaCl8 小时前
如何在 IDEA 中使用 Proguard 自动混淆 Gradle 编译的Java 项目
android·java·经验分享·kotlin·gradle·intellij-idea
旷野说10 小时前
Android Studio Narwhal 3 特性
android·ide·android studio
maki07716 小时前
VR大空间资料 01 —— 常用VR框架对比
android·ue5·游戏引擎·vr·虚幻·pico
xhBruce20 小时前
InputReader与InputDispatcher关系 - android-15.0.0_r23
android·ims
领创工作室20 小时前
安卓设备分区作用详解-测试机红米K40
android·java·linux
hello_ludy20 小时前
Android 中的 mk 和 bp 文件编译说明
android·编译
maki0771 天前
VR大空间资料 03 —— VRGK使用体验和源码分析
android·vr·虚幻·源码分析·oculus·htc vive·vrgk