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

相关文章:

相关推荐
每次的天空1 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭1 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日2 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安2 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑3 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟7 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡8 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi008 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体
zhangphil10 小时前
Android理解onTrimMemory中ComponentCallbacks2的内存警戒水位线值
android
你过来啊你10 小时前
Android View的绘制原理详解
android