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

相关文章:

相关推荐
编程乐学(Arfan开发工程师)2 小时前
06、基础入门-SpringBoot-依赖管理特性
android·spring boot·后端
androidwork2 小时前
使用 Kotlin 和 Jetpack Compose 开发 Wear OS 应用的完整指南
android·kotlin
繁依Fanyi3 小时前
Animaster:一次由 CodeBuddy 主导的 CSS 动画编辑器诞生记
android·前端·css·编辑器·codebuddy首席试玩官
奔跑吧 android5 小时前
【android bluetooth 框架分析 02】【Module详解 6】【StorageModule 模块介绍】
android·bluetooth·bt·aosp13·storagemodule
田一一一9 小时前
Android framework 中间件开发(三)
android·中间件·framework·jni
androidwork13 小时前
掌握 Kotlin Android 单元测试:MockK 框架深度实践指南
android·kotlin
田一一一14 小时前
Android framework 中间件开发(二)
android·中间件·framework
追随远方14 小时前
FFmpeg在Android开发中的核心价值是什么?
android·ffmpeg
神探阿航15 小时前
HNUST湖南科技大学-安卓Android期中复习
android·安卓·hnust
千里马-horse17 小时前
android vlc播放rtsp
android·media·rtsp·mediaplayer·vlc