Android APP 热修复原理

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

dexElements

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

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

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


http://aospxref.com/android-10.0.0_r47/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java#51

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


http://aospxref.com/android-10.0.0_r47/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java#69

findClass 方法迭代 dexElements 数组查找类


http://aospxref.com/android-10.0.0_r47/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

热修复原理

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

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

代码实现

  1. 通过反射拿到 ClassLoader 中的 dexElements 数组

    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
     }

    }

  2. 合并两个 ClassLoader 中的 dexElements 数组

    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

    }

  3. 替换掉 PathClassLoader 中的 dexElements

    @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 类源码如下:

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

class PluginClass {

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

}

编译 apk

把 apk 推送到设备 sdcard

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

2. app

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

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

class PluginClass {

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

}

热修复代码:

复制代码
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方法,并显示结果

复制代码
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

复制代码
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 ===');
            }
        });
    });
}

执行脚本

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

枚举所有存在的 ClassLoader 实例

复制代码
printAllClassLoaders()

日志输出如下:

复制代码
[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

完整源码

开源地址:https://github.com/CYRUS-STUDIO/AndroidExample

相关文章:

相关推荐
xiangpanf14 分钟前
Laravel 10.x重磅升级:五大核心特性解析
android
robotx3 小时前
安卓线程相关
android
消失的旧时光-19434 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon4 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon5 小时前
VSYNC 信号完整流程2
android
dalancon5 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013846 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android6 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才7 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶7 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle