AndFix是阿里巴巴的开源软件,缺点:4年没更新了,兼容性问题。
开源地址
Tinker原理的核心是:dex的替换,java层的,不支持即时生效,需要应用冷启动。
AndFix原理核心:方法的替换,native层替换,支持即时生效。
核心:native层结构体ArtMethod,记录了方法所有的信息,包括方法属于的类、访问权限、代码执行入口等信息。
下面需要学习,为何替换结构体ArtMethod就能实现方法的修改,怎么替换结构体。
一、基础知识
网上查看android源码地址:
http://androidxref.com
1、Android编译过程:
.java -> .class -> .dex
虚拟机中解释器模式或AOT快速编译模式来执行dex
在ArtMethod里面有一个结构体PtrSizedFields,描述了一个方法的各种入口,包括解释器模式下的入口、AOT快速编译模式的入口等。
// Must be the last fields in the method.
struct PACKED(4) PtrSizedFields {
// Method dispatch from the interpreter invokes this pointer which may cause a bridge into
// compiled code.
void* entry_point_from_interpreter_;
// Pointer to JNI function registered to this method, or a function to resolve the JNI function.
void* entry_point_from_jni_;
// Method dispatch from quick compiled code invokes this pointer which may cause bridging into
// portable compiled code or the interpreter.
void* entry_point_from_quick_compiled_code_;
// Method dispatch from portable compiled code invokes this pointer which may cause bridging
// into quick compiled code or the interpreter. Last to simplify entrypoint logic.
#if defined(ART_USE_PORTABLE_COMPILER)
void* entry_point_from_portable_compiled_code_;
#endif
} ptr_sized_fields_;
2、查找方法:双亲委托机制
双亲委托
1、汇编器
直接将汇编语言翻译为机器码,速度最快。
2、编译器
将高级语言编译成汇编语言,然后用汇编器转换为机器码。
编译慢,但是运行快。
编译器编译出来的代码是平台相关的。
3、解释器
执行时才翻译代码,速度慢。
Java运行
为了是代码平台无关,java提供了JVM,java虚拟机。JVM是平台相关的。
java编译器将java编译为.class字节码,然后给JVM翻译为机器码。
Android代码运行
.java->.class->.dex 字节码
dex字节码通过art或dalvik runtime转换为机器码。
Dalvik是基于JIT编译的引擎。
从Android4.4后,引入了ART作为运行时,从5.0后ART全面替代dalvik。
7.0后在ART中添加了一个JIT编译器,可以在运行时持续提高性能。
Dalvik使用JIT编译,ART使用AOT编译。
JIT是程序运行时实时将Delvik字节码翻译成机器码,AOT是在程序安装时将dex翻译为机器码。
下面就需要看如何改变artmethod里方法入口指针,指向新的方法体。
二、源码阅读:类是如何加载和查找到的
1、FindClass
在JNI里有一个方法, env->FindClass(methodName);
那如何查看该方法的实现源码呢?
jni的实现源码在:
http://androidxref.com/5.1.1_r6/xref/art/runtime/jni_internal.cc
具体过程省略,整个查找流程为:
1、JNI FindClass
2、native FindClass
jni_internal.cc 589
3、ClassLinker FindClass()
class_linker.cc 2117
4、LookupClass() 双亲委托
class_linker.cc 3348
5、DefineClass()
class_linker.cc 2218
6、LoadClassMembers()
class_linker.cc 2767
7、LinkCode()
class_linker.cc 2627
8、GetOatMethod()
oat_file.cc 595
9、NeedInterperter() 2525
10、UpdateMethodsCode()
instrumentation.cc 679
三、源码阅读:方法是如何找到的
Android运行时虚拟机启动的入口方法:
AndroidRuntime.cpp
//Start the Android runtime. This involves starting the virtual machine
// and calling the "static void main(String[] args)" method in the class
// named by "className".
void AndroidRuntime::start(const char* className, const Vector<String8>& options){}
虚拟机启动的第一个类是什么:叫做ZygoteInit类。
[ZygoteInit.java](http://androidxref.com/5.1.1_r6/xref/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java)
在start()方法中,启动了Java虚拟机(987行)。
在1027行加载了ZygoteInit类的main()方法。
调用env->GetStaticMethodID方法,该方法是jni_internal.cc里的方法,就是查找类里的方法,然后将art_method强转为jmethodId。
手写实现AndFix
项目代码在GitHub上。
https://github.com/Hujunjob/HotFix
首先生成一个需要替换方法的类。
//错误方法
class Calculator {
fun calculator(context: Context) {
val a = 100
val b = 0
Toast.makeText(context, "calculator a/b = ${a / b}", Toast.LENGTH_SHORT).show()
}
}
修改方法:
class Calculator_Fix {
@MethodReplace(className = "com.hujun.hotfix.Calculator",methodName = "calculator")
fun calculator(context: Context) {
val a = 100
val b = 1
Toast.makeText(context, "calculator a/b = ${a / b}", Toast.LENGTH_SHORT).show()
}
}
其中,自定义注解是用来注解用来替换的方法。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodReplace {
//要替换的类名
String className();
//要替换的方法名
String methodName();
}
然后生成一个dex替换文件。
在build/tmp/kotlin-classes/debug下找到需要替换的.class文件。
找到dex工具。
在Android的SDK里面,/Users/junhu/Library/Android/sdk/build-tools,这是我的Android SDK存储路径。
然后找到dx文件,配置到环境变量里。
dx --dex --output 需要生成dex的目录和名称 class文件所在的目录
如果就在class文件所在的目录里,则用.即可
dx --dex --output out.dex .
将生成的out.dex拷贝到/sdcard里,待会用这个dex文件来做热更新。
写一个Dex管理类,用来加载和热替换曾经存在的方法。
object DexManager {
// private val TAG = this::class.java.name.replace("${'$'}Companion","").split(".").last()
fun loadDex(path: String, context: Context) {
//loadDex(String sourcePathName, String outputPathName, int flags)
val dexFile =
DexFile.loadDex(path, File(context.cacheDir, "opt").absolutePath, Context.MODE_PRIVATE)
if (dexFile != null) {
for (entry in dexFile.entries()) {
val clazz = dexFile.loadClass(entry, context.classLoader)
if (clazz != null) {
fixClass(clazz)
}
}
}
}
private fun fixClass(clazz: Class<Any>) {
for (method in clazz.declaredMethods) {
val annotation = method.getAnnotation(MethodReplace::class.java)
if (annotation != null) {
val className = annotation.className
val methodName = annotation.methodName
if (TextUtils.isEmpty(className) || TextUtils.isEmpty(methodName)) {
continue
}
val clz = Class.forName(className)
val bugMethod = clz.getDeclaredMethod(methodName, *method.parameterTypes)
JNI.replaceMethod(bugMethod,method)
// Log.i("dexmanager", "fixClass: bugmethod=${bugMethod.name},fixedmethod=${method.name},bug class=${clz.name},fixed class=${clazz.name}")
}
}
}
}
其中JNI.replaceMethod()是调用jni方法,其在native层的实现如下:
#include "art_5_1.h"
using namespace art::mirror;
/*
* Class: com_hujun_hotfix_JNI
* Method: replaceMethod
* Signature: (Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V
*/
extern "C" JNIEXPORT void JNICALL Java_com_hujun_hotfix_JNI_replaceMethod
(JNIEnv *env, jobject, jobject bugMethod_, jobject fixedMethod_) {
ArtMethod *bugMethod = reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(
bugMethod_));
ArtMethod *fixedMethod = reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(
fixedMethod_));
//首先获得原来坏掉的方法的ClassLoader
((Class *) fixedMethod->declaring_class_)->class_loader_ =
((Class *) bugMethod->declaring_class_)->class_loader_;
//线程id也赋值下
((Class *) fixedMethod->declaring_class_)->clinit_thread_id_ =
((Class *) bugMethod->declaring_class_)->clinit_thread_id_;
//为啥状态要减一
((Class *) fixedMethod->declaring_class_)->status_ =
((Class *) bugMethod->declaring_class_)->status_ - 1;
((Class *) fixedMethod->declaring_class_)->super_class_ = 0;
//成员替换
bugMethod->declaring_class_ = fixedMethod->declaring_class_;
bugMethod->dex_cache_resolved_methods_ = fixedMethod->dex_cache_resolved_methods_;
bugMethod->access_flags_ = fixedMethod->access_flags_;
bugMethod->dex_cache_resolved_types_ = fixedMethod->dex_cache_resolved_types_;
bugMethod->dex_code_item_offset_ = fixedMethod->dex_code_item_offset_;
bugMethod->method_index_ = fixedMethod->method_index_;
bugMethod->dex_method_index_ = fixedMethod->dex_method_index_;
bugMethod->ptr_sized_fields_.entry_point_from_interpreter_ =
fixedMethod->ptr_sized_fields_.entry_point_from_interpreter_;
bugMethod->ptr_sized_fields_.entry_point_from_jni_ =
fixedMethod->ptr_sized_fields_.entry_point_from_jni_;
bugMethod->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
// fixedMethod->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
}
其中的 "art_5_1.h"头文件是art_method头文件,阿里进行了简化,可以直接在AndFix里获取,不要自己写了。
https://github.com/alibaba/AndFix/tree/master/jni/art
native层的代码是核心代码,将老的有bug的方法,重新定向其入口,指向修复过的方法的入口。并且将老方法的其他参数赋值给新方法,比如把classloader给新方法。
这样在FindClass()时(前面说的双亲委托),在同一个ClassLoader下,调用老方法时,实际调用的就是新方法了。
由此实现了方法的热更新。
这里有个问题需要注意,FindClass()时,如果老方法所在的类并没有被加载过,那替换无法成功,因为并没有被放入ClassLoader的缓存里。
老方法所在的类,在热更新前,务必已经进行new出来过了。
AndFix的缺点
最大的缺点就是,兼容性问题。
1、Android每次升级,ArtMethod的代码都可能被修改,这样在热更新时,很可能替换就失败了。
2、由于Android是开源的,所有手机厂商都可以对Android代码进行修改,如果修改到了ArtMethod的代码,也会导致热更新替换失败。