当前位置: 首页 > news >正文

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 的内部类,负责构建和维护类加载搜索路径。

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

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

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

findClass 方法迭代 dexElements 数组查找类

word/media/image3.png
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 = trueval pathList = pathListField.get(classLoader)// 2. 拿到 dexElements 字段val pathListClass = pathList.javaClassval 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 数组
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.sizeval 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
@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方法,并显示结果

word/media/image4.png

1. plugin

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

word/media/image5.png

PluginClass 类源码如下:

package com.cyrus.example.pluginclass PluginClass {fun getString(): String {return "String from plugin."}}

编译 apk

word/media/image6.png

把 apk 推送到设备 sdcard

adb push plugin-debug.apk /sdcard/Android/data/com.cyrus.example/files

2. app

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

package com.cyrus.example.pluginclass 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.

word/media/image7.png

点击热修复按钮

word/media/image8.png

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

word/media/image9.png

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

相关文章:

  • Android 下的 ClassLoader 与 双亲委派机制

  • 详解 Android APP 启动流程

  • Android 加壳应用运行流程 与 生命周期类处理方案

相关文章:

  • java.lang.ArrayIndexOutOfBoundsException: 11
  • 时间序列预测模型比较分析:SARIMAX、RNN、LSTM、Prophet 及 Transformer
  • 51单片机中断
  • Electron从入门到入门
  • Nacos简介—2.Nacos的原理简介
  • Linux:进程间通信->匿名管道实现内存池
  • 深入剖析 Vue 双向数据绑定机制 —— 从响应式原理到 v-model 实现全解析
  • Android中的多线程
  • ubuntu20.04安装x11vnc远程桌面
  • 如何成功防护T级超大流量的DDoS攻击
  • 【Leetcode 每日一题】2845. 统计趣味子数组的数目
  • 汽车售后 D - PDU 和 J2543 详细介绍
  • 驱动开发硬核特训 · Day 21(下篇): 深入剖析 PCA9450 驱动如何接入 regulator 子系统
  • Serverless 在云原生后端的实践与演化:从函数到平台的革新
  • classfinal 修改过源码,支持jdk17 + spring boot 3.2.8
  • 【k8s】sidecar边车容器
  • 项目maven版本不一致 导致无法下载
  • 【遥感图像分类】【综述】遥感影像分类:全面综述与应用
  • python实现简单的UI交互
  • redis客户端库redis++在嵌入式Linux下的交叉编译及使用
  • 四川省人大常委会原党组成员、副主任宋朝华接受审查调查
  • 远程控制、窃密、挖矿!我国境内捕获“银狐”木马病毒变种
  • 往事|学者罗继祖王贵忱仅有的一次相见及往来函札
  • 上海银行一季度净赚逾62亿增2.3%,不良贷款率与上年末持平
  • “养老规划师”实则售卖保险,媒体:多部门须合力整治乱象
  • “70后”女博士张姿卸任国家国防科技工业局副局长