Andfix
andfix从native入手修改ArtMethod的字节码地址实现错误方法块的修复。修复的粒度是方法块字节码引用。
Java的内存分布
方法区
JVM读取class文件,提取class的类型信息,并将这些信息存储到方法区,同时该类型的类静态变量也会放到方法区,还有方法表,每个类都会有个方法表。
堆区
主要存放的是对象,Java程序在运行时创建的所有类型的对象和数组都储存在堆中。
JVM会根据new的指令在堆中开辟一个确定类型的对象内存空间,但是堆中开辟的对象空间并没有任何人工指令可以回收,而是通过JVM的垃圾回收机制进行回收。
栈区
每启动一个线程,JVM都会为它创建一个JAVA栈,用于存放方法中的局部变量、操作数等。
当线程调用某个方法时,JVM会根据方法区中该方法的字节码组建一个栈帧,并将该栈帧压入到Java栈中,方法执行完毕后,JVM将该栈帧弹出,并释放掉。
我们写的Java文件被编译成class文件到最终加载到内存,所属的内存区域是方法区,所以要做的就是,将差分包中没有bug的方法替换掉已经被加载到内存方法区中有bug方法。
虚拟机运行机制
- 声明一个ApplicationThread类型的成员变量
//这一步不会将Application.class字节码加载到内存的,会在方法区生成一个int类型的符号表量
private Application application
对象只有在主动引用的情况下才会加载到内存,常见的主动引用方式有:new、反射创建、JNI的findCalss()、序列化、调用类的静态成员变量(final除外)和静态方法、初始化一个类如果其父类没有初始化,会先初始化父类。
-
通过反射的方式创建application对象,这一步会将application的字节码文件加载到内存,创建的对象存储在堆区。
当通过Application对象调用onCreate()方法时,堆区的application对象指向int符号变量,int符号变量指向方法表,执行onCreate()方法,将onCreate组建成栈帧,压入Java栈,执行完毕后弹出并释放。
方法表
方法表可以理解为是一个数组,数组中存放的是ArtMethod结构体(Android5.0之前为Method)。ArtMethod中存放的方法入口、字节码地址等信息。
扩展JVM DVM ART
- JVM基于栈,意味着需要去栈中读写数据,所需要的指令会更多,这样会导致速度变慢,对于性能有限的移动设备显然不合适。
- DVM是基于寄存器的,它没有基于栈的虚拟机在复制数据时而使用大量的出入栈指令,同事指令更紧凑、简洁。但是由于显式的制定了操作数,所以基于寄存器的指令会比基于栈的指令要大。
- JVM中,java类被编译成一个或多个.class文件,并打包成.jar文件,而后JVM会通过相应的.class文件和.jar文件获取相应的字节码。
而DVM会用dx工具把所有的class文件打包成一个.dex文件,然后DVM会从该.dex文件中读取指令和数据。这个.dex文件将所有的.class文件里面所包含的信息全部整合到了一起,这样加载就加快了速度。 - DVM经过优化,允许在有限的内存中同时运行多个进程。在Android中每一个应用都运行在一个DVM实例中,每一个DVM实例都运行在一个独立的进程空间,独立的进程可以防止虚拟机崩溃时所有程序都被关闭。
- Zygote是一个DVM进程,同时也用来创建和初始化其他DVM进程。每当系统需要一个应用程序进程的时候,Zygote就会fork自身,快速地创建和初始化一个DVM实例,用于程序运行。
- DVM拥有预加载-共享机制,不同应用之间运行时可以共享相同的类,拥有更高的效率。而JVM机制不存在这种共享机制。不同的程序,打包以后程序都是彼此独立的,即便是他们使用了相同的类,运行时也都是单独加载和运行的。
- JVM使用了JIT(Just In Time Compiler),而DVM早期没有使用JIT编译器。早期DVM执行代码,都需要解释器将dex代码编译成机器码,然后交给系统处理,效率不是很高。Android 2.2之后开始为DVM使用了JIT编译器,它会对多次运行的代码(热点代码)进行编译,生成相当精简的本地机器码(Native Code),这样在下次执行到相同的逻辑时,直接使用编译好的机器码即可。需要注意的是,应用程序每次重新运行的时候,都需要重做这个编译工作。
ART虚拟机
前文了解到,DVM中的应用每次运行时,字节码都需要通过JIT编译器编译为机器码,这样会使应用程序的运行效率降低。而在ART中,系统安装应用程序时会进行一次AOT(ahead of time compilation),将字节码预编译成机器码并存储在本地,这样应用程序每次运行时就不需要执行编译了,会大大增加效率。但是AOT不是完美的,它的缺点主要有两个:第一个是AOT会使安装应用的时间变长,尤其是复杂的应用。第二个是字节码预先编译成机器码,机器码需要存储空间会多一些。为了解决这两个问题,Android 7.0版本中的ART加入了JIT即时编译器,作为AOT的一个补充。应用程序安装时并不会将字节码全部编译成机器码,而是在系统运行中将热点代码编译成机器码,从而缩短应用程序安装时间,并且节省内存。
DVM是为32位CPU设计的,而ART是支持64位并且兼容32位CPU,这也是DVM被淘汰的主要原因之一。
ART对垃圾回收机制进行了改进,比如更频繁的执行并行垃圾收集,将GC暂停由2次减少为1次等等。
ART运行时堆空间划分和DVM不同。
手动实现Andfix
- 找到要修复的方法块,修复bug代码后,对方法加上注解方便后续定位到修改的方法。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
//a
public @interface Replace {
// 目的
// 地方 在哪里 14号 多少钱
String clazz();
String method();
}
- 编译修改的Java文件生成.class,使用SDK的DX工具转成.dex文件放入手机中。
- APP加载dex文件,遍历出dex中所有的Class后,使用之前的注解找到要修改的function
DexFile dexFile = DexFile.loadDex()
Enumeration<String> entry= dexFile.entries();
while (entry.hasMoreElements()) {
// 全类名
String className = entry.nextElement();
Class realClazz=dexFile.loadClass(className, context.getClassLoader());
if (realClazz != null) {
fixClass(realClazz);
}
}
private void fixClass(Class realClazz) {
//加载方法 Method
Method[] methods = realClazz.getMethods();
for (Method rightMethod : methods) {
Replace replace = rightMethod.getAnnotation(Replace.class);
if (replace == null) {
continue;
}
String clazzName = replace.clazz();
String methodName = replace.method();
try {
Class wrongClazz=Class.forName(clazzName);
//Method right wrong
Method wrongMethod=wrongClazz.getDeclaredMethod(methodName, rightMethod.getParameterTypes());
replace(wrongMethod,rightMethod);
} catch (Exception e) {
e.printStackTrace();
}
}
//调用到Native层,进行ArtMethod结构体的修改
private native static void replace(Method wrongMethod,Method rightMethod);
- 赋值android源码中art_method.h,只保留部分声明即可。
extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_andfix_DexManager_replace(JNIEnv *env, jobject instance, jint sdk,jobject wrongMethod,
jobject rightMethod) {
// ArtMethod ----->
art::mirror::ArtMethod *wrong= reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(wrongMethod));
art::mirror::ArtMethod *right= reinterpret_cast<art::mirror::ArtMethod *>(env->FromReflectedMethod(rightMethod));
// wrong=right;
wrong->declaring_class_ = right->declaring_class_;
wrong->dex_cache_resolved_methods_ = right->dex_cache_resolved_methods_;
wrong->access_flags_ = right->access_flags_;
wrong->dex_cache_resolved_types_ = right->dex_cache_resolved_types_;
wrong->dex_code_item_offset_ = right->dex_code_item_offset_;
wrong->dex_method_index_ = right->dex_method_index_;
wrong->method_index_ = right->method_index_;
}
它的思想是修改错误ArtMethod结构体中方法入口和字节码地址等,为正确ArtMethod的结构体中对应的内容。这样堆内存对象指向方法时,会去方法区找到ArtMethod,因为ArtMethod的字节码地址修改了,往栈内存压栈的栈帧就会是正确的字节码内容,从而bug得到修复。
- 实际开发中,修复bug不需要特意在修复的方法上添加注解。Andfix提供了差分包制作工具,能够比对修改前后的apk,找出修改了的方法块,再生成一个dex,使用jadx查看可以发现,dex里面有一个该工具创建的class类,包含了修改了的方法,方法上有一个类似Replace 的注解。
Tinker
tinker利用Java层的ClassLoader机制,将修复包中的dex插入到系统ClassLoader的dexElements前端,系统取到了我们替换的class。修复的粒度是整个类替换。
原理和动态加载apk一样。
参考:https://www.jianshu.com/p/fd9ed8b720ef
Andfix和Tinker的差异
由于执行方法时虚拟机层都要读取方法表然后组装栈帧压栈,Andfix可以实现实时热修复。Tinker利用的是ClassLoader机制,但是当一个类已经被加载后,ClassLoader会将其缓存在内存中,不会再去读取dexElement了,所以Tinker不能实现实时热修复。