replacemethod必须区分art和dalvik,因二者底层方法结构体(artmethod/dvmmethod)内存布局、字段名及偏移完全不兼容,错用版本会导致crash或补丁失效。

为什么 replaceMethod 必须区分 ART 和 Dalvik?
因为 Java 方法在虚拟机里不是“调用名字就执行”,而是靠一个叫 ArtMethod(ART)或 DvmMethod(Dalvik)的底层结构体控制跳转。这个结构体的内存布局、字段偏移、甚至字段名,在不同虚拟机和 Android 版本间完全不兼容。
比如 Android 5.0 的 ArtMethod 里,入口函数指针存在 entry_point_from_interpreter_ 字段;而到了 Android 7.0,它被拆成两个字段:ptr_sized_fields_.entry_point_from_interpreter_ 和 ptr_sized_fields_.entry_point_from_jni_。如果用错版本的替换逻辑,直接写错内存地址,轻则补丁失效,重则 crash。
-
isArt判断不能只看 API level,得结合Build.VERSION.SDK_INT和运行时Runtime.getRuntime().availableProcessors()等辅助信号交叉验证 - Android 4.4 是分水岭:Dalvik → ART 过渡期,部分机型双虚拟机共存,需额外检测
System.getProperty("java.vm.name") - NDK 编译时必须为每个 target ABI(arm64-v8a / armeabi-v7a)单独编译对应
replace_*.cpp,否则art_replaceMethod调用会 segfault
如何安全拿到 src 和 dest 对应的 ArtMethod*?
关键不是“能不能拿到”,而是“拿得准不准”。env->FromReflectedMethod(src) 返回的是一个指针,但它指向的内存区域是否可读写、是否已被 GC 移动、是否属于系统类(如 java.lang.String),都会让后续赋值失败。
常见错误现象:NullPointerException 在 native 层没抛出,但 Java 层方法调用后行为不变——其实是 smeth 指针为空或非法。
- 必须在
src和dest都已通过Class.forName()加载、且未被ClassLoader卸载的前提下调用,否则FromReflectedMethod返回空 - 对
dest所在类,要确保其ClassLoader已加载过该类(补丁 dex 必须已插入到PathClassLoader的dexElements前端) - Android 8.0+ 引入了
hiddenapi限制,反射访问ArtMethod字段会被 block,需在AndroidManifest.xml中声明android:usesCleartextTraffic="true"并绕过HiddenApiBypass检查(非正式环境慎用)
替换 ArtMethod 字段时哪些字段绝对不能漏?
不是所有字段都影响执行流。漏掉 access_flags_ 可能导致方法不可见;漏掉 declaring_class_ 会让反射获取类信息出错;但真正决定“调哪段代码”的,只有两个字段:
-
ptr_sized_fields_.entry_point_from_interpreter_:解释执行路径入口(绝大多数 Java 方法走这里) -
ptr_sized_fields_.entry_point_from_jni_:JNI 调用路径入口(涉及native方法时才需同步) - 其他字段如
dex_code_item_offset_、dex_method_index_影响调试符号和异常堆栈还原,不影响运行逻辑,可暂不替换
注意:Android 10 开始,entry_point_from_interpreter_ 默认指向 JIT 编译后的代码地址,若补丁方法未 JIT,需先触发一次解释执行再替换,否则跳转到野指针。
热修复后为什么有些方法仍调旧实现?
最常被忽略的一点:ART 的 inline 缓存(inline cache)和 JIT 编译缓存不会自动失效。即使你替换了 ArtMethod,JIT 已经把原方法内联进调用方字节码,那调用方压根不会再去查 ArtMethod。
典型场景:Activity 的 onCreate() 调用了 Utils.fixBug(),而后者被热修复。但如果 fixBug() 在首次启动时已被 JIT 内联,后续即使替换了它的 ArtMethod,onCreate() 里执行的仍是旧逻辑。
- 必须在补丁生效后,主动触发
System.runFinalization()+Runtime.getRuntime().gc()清理 JIT 缓存(仅限 debug 环境,release 不建议) - 更稳妥的做法是:补丁方法避免被高频调用或内联(加
@Keep+final+ 减少参数数量) - Android 12+ 提供了
Runtime.getRuntime().deoptimizeBootImage(),但仅限 system app 使用
真正的难点不在“怎么换”,而在“换完系统认不认”。ART 的优化机制越强,热修复的不确定性越高——这也是为什么现在主流方案(Tinker、Sophix)默认放弃方法级即时生效,转向重启类加载器的妥协路径。










