热修复
目前国内Android热修复主流框架有阿里的Andfix
,Sopfix
,微信的Tinker
,美团的Robsut
等等等等,就不一一列举了...
其中Andfix
和Sopfix
是用过native
层实现的,Tinker
和Robsut
是通过Java
层实现的
附主流框架图示:
两种实现方式各有优劣,这里不做过多分析,本文仅简单分析Andfix的基础实现
流程图
Andfix实现热修复的原理流程图大致如下:
简单说就是:首先我们定位到出现bug的类的方法,修复打一个新的安装包,与旧的安装包(bug)作比较,生成差分包(修复包),替换旧app的出现bug的方法
定位bug → 打新包 → 比较生成差分包 → 替换有bug的方法
内存分布
要搞明白热修复的原理之前,我们需要对Java的内存分布有所了解,如图:
方法区:
当JVM使用类加载器定位class文件,并将其输入到内存中,会提取class的类型信息,并将这些信息存储到方法区,同时该类型的类静态变量也会放到方法区,还有方法表
,每个类都会有个方法表。
堆区:
堆区主要存放的是对象,Java程序在运行是创建的所有类型的对象和数组都存储在堆中。
JVM会根据new
的指令在堆中开辟一个确定类型的对象内存空间,但是堆中开辟对象的空间并没有任何人工指令可以回收,而是通过JVM的垃圾回收器进行回收。
栈区:
每启动一个线程,JVM都会为它创建一个Java栈,用于存放方法中的局部变量,操作数以及异常数据等。
当线程调用某个方法时,JVM会根据方法区中该方法的字节码组建一个栈帧,并将该栈帧压入到Java栈中,方法执行完毕后,JVM将该栈帧弹出,并释放掉。
首先我们要明白,我们写的Java文件被编译生成.class文件到最终加载到内存,所属的内存区域是方法区
,所以我们要做的是就是,将差分包中没有bug的方法替换掉已经被加载到内存方法区
中的有bug方法。
所以,我们要替换的是运行在Java虚拟机中的java方法,当然我们通过java技术是不能实现这个步骤的,所以Andfix使用native替换java方法
,这里我们需要简单说明一下Java的虚拟机运行机制
虚拟机运行机制
当用户手指点击App时,Launcher
会告诉JVM
加载执行对应的ActivityThread
类,流程大概如下:
其中文件加载(ActivityThread.class)是通过DexFile.java
完成的,内部通过native
方法实现,流程大致如下:
当ActivityThread
的main
方法执行,会加载执行App的Application
类,通过反射的方式创建一个Application对象,然后调用它的onCreate()
方法
我们将这个步骤拆分开
第一步:声明一个Application类型的成员变量()
//举例
//这一步是不会将Application.class字节码加载到内存的,会在方法区生成一个int类型的符号变量
private Application application;
补充: 对象只有在主动引用
的情况下才会加载到内存,常见的主动引用的方式有:new一个对象、反射创建对象、JNI的findClass() 、序列化、调用类的静态成员变量(final除外)和静态方法、初始化一个类如果其父类没有初始化,会先初始化父类
第二步:通过反射的方式创建Application对象,这一步才会将Application的字节码文件加载到内存,创建的对象式存储在堆区
的
然后通过Application对象调用onCreate()
方法,我们看下流程图:
如上图所述:在堆区的对象会指向int类型的符号变量
这也是为什么我们创建一个对象能够getClass()
方法获取对象(native实现,使用klass变量)
当通过Application对象调用onCreate()
方法时,堆区的application对象指向int符号变量
,int符号变量指向方法表
,执行onCreate()方法,将onCreate组建成栈帧,压入Java栈,执行完毕后弹出并释放~
看了一堆流程图,是不是有很多问号???
了解上述原理才能更好的理解Andfix的实现原理,切入正题之前还有一个问题,如果我们的某个类的一个方法出现了异常(程序执行可能创建多个该类型的对象,都有可能调用该方法
),我们应该在哪个区域切入实现修改替换有异常的方法后所有对象调用该方法都不会出异常? 答案是:方法表
,因为所有的对象执行方法都会通过符号变量找到方法表,然后在将方法组建成栈帧压栈执行
ArtMethod结构体
方法表可以理解为是一个数组,数组中存放的是ArtMethod结构体
,加载类信息创建方法表,实例化ArtMethod结构体对象的步骤,在Android源码中藏得比较深(重点是看不懂~),有兴趣的可以看下,这里以Android5.x版本为例,路径在android-5.0.1_r1
→art
→runtime
目录下,找到class_linker.cc
文件然后依次看FindClass(主要实现双亲委托机制)
→DefineClass(主要定义一个Class,上文提到的klass在这个方法中初始化)
→LineClass(从硬盘中加载类信息,先加载父类(LineSuperClass))
→LinkInterfaceMethod()
,最终在LinkInterfaceMethod()
中创建方法表,根据方法个数遍历,实例化ArtMethod结构体
,存放在数组中
Andfix基础功能代码实现
Android studio创建一个JNI项目
创建几个类大致如下图:
-
Replace
注解 用于表示需要修复的类名方法名
/**
* 注解,标注需要修复的类名,方法名等信息
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Replace {
String clazz(); //修复哪个类
String method(); //修复哪个方法
}
- 与
MainActivity
同级的Calculator
为模拟异常信息的类,只有一个calculator
方法,直接跑了一个异常
/**
*模拟异常
*/
public class Calculator {
public int calculator(){
//直接抛了一个RuntimeException
throw new RuntimeException();
}
}
3service
目录下的Calculator
类是为了生成差分包创建的,省去了手动打差分包,借助Android studio的build功能生成.class文件
/**
*模拟修复后的类
* 正常情况下应该是从后端获取修复后的Calculator.java编译之后的.dex文件,写在这里省略了手动编译打包的过程
* Calculator.java通过JavaC命令编译成Calculator.class, 然后可以通过dex命令打包成.dex文件
*/
public class Calculator {
@Replace(clazz = "com.jni.fixdemo.Calculator" ,method = "calculator")
public int calculator(){
return 1024;
}
}
需要通过这个类生成差分包,也就是.dex文件,点击rebuild
,会在app\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\包名
目录下生成.class文件,然后通过dx.bat
文件打包生成.dex
文件,dx.bat
文件在SDK
目录下的\build-tools\版本号\
目录下
生成.dex文件
有两种方式,
第一种:将上述文件copy到dx.bat
所在目录,执行dx的命令,
还有一种是将dx.bat
所在目录添加到系统环境变量,在cmd
下执行dx命令
dx --dex --output = C:\Users\tpson\Desktop\out\fix.dex C:\Users\tpson\Desktop\result
注意 : 这里的C:\Users\tpson\Desktop\out\fix.dex
表示输出路径以及文件名,后面的C:\Users\tpson\Desktop\result
表示.class文件
的路径,这里不是全路径
,
比如我的.class文件实际在C:\Users\tpson\Desktop\result\com\jni\fixdemo\service\Calculator.class
,执行命令如上
-
MainActivity
主要就是一个TextView
,两个Button
,其中一个button点击事件调用异常方法,另一个button点击事件,调用替换异常方法的逻辑,比较简单不贴代码了,可以移步github查看完整代码Andfix基础功能实现
-
DexManager
主要作用是从SD卡中加载差分包(.dex文件,三方工具生成的可能是.patch等后缀文件),利用Replace
注解找到需要替换的方法,交给native层实现
这里需要注意: fix.dex文件
是从SD加载的,所以不能通过Class.forName()
方法加载类信息,而是通过DexFile.loadClass()
方法获取类信息,当我们从fix.dex文件
中获取到需要替换的方法以及所属的类名时候,加载需要被替换的方法所属的类可以通过Class.forName()
加载类信息,因为这个类是通过虚拟机加载到内存中的
/**
* 加载.dex文件
*/
public class DexManager {
private Context context;
public DexManager(Context context) {
this.context = context;
}
public void load(File file){
try {
DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(), new File(context.getCacheDir(), "opt").getAbsolutePath(), Context.MODE_PRIVATE);
//拿到当前dex文件下所有类名集合
Enumeration entries = dexFile.entries();
while (entries.hasMoreElements()){
String clazzName=entries.nextElement();
//这里需要注意,dex文件是从sd加载的,所以不能直接使用Class.forName反射的方式加载类信息
//通过DexFile.loadClass加载类信息
Class clazz = dexFile.loadClass(clazzName, context.getClassLoader());
if (clazz!=null) {
fixClazz(clazz);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void fixClazz(Class clazz) {
//目的是替换方法,遍历类中的所有方法
Method[] methods = clazz.getMethods();
for (Method fixMethod : methods) {
//通过注解,确定要修改的方法
Replace replace = fixMethod.getAnnotation(Replace.class);
if (replace==null) {
continue;
}
String clazzName = replace.clazz();
String methodName = replace.method();
try {
//通过反射的方式拿到原来的类(有bug的类)的信息
Class errorClass = Class.forName(clazzName);
//拿到需要替换的方法,第二个参数表示参数列表一致,保证找到正确的方法
Method errorMethod = errorClass.getDeclaredMethod(methodName, fixMethod.getParameterTypes());
replace(errorMethod,fixMethod);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//声明native方法,在native-lib.cpp中实现替换方法的逻辑
public native static void replace(Method errorMethod,Method fixMethod);
}
头文件和native层替换方法的实现
注意:
因为Andfix实现的方式决定,其在兼容性上有很大的问题,所以下面的代码是基于Android6.0的,也就是在Android6.0
的机型上可以正常运行,这里直接参考了Andfix源码的头文件和native实现
头文件art_method.h
#include
#include
#include
#include
#include
#include /* C99 */
namespace art {
namespace mirror {
class Object {
public:
// The number of vtable entries in java.lang.Object.
static constexpr size_t kVTableLength = 11;
static uint32_t hash_code_seed;
uint32_t klass_;
uint32_t monitor_;
};
class Class: public Object {
public:
static constexpr uint32_t kClassWalkSuper = 0xC0000000;
static constexpr size_t kImtSize = 0; //IMT_SIZE;
uint32_t class_loader_;
uint32_t component_type_;
uint32_t dex_cache_;
uint32_t dex_cache_strings_;
uint32_t iftable_;
uint32_t name_;
uint32_t super_class_;
uint32_t vtable_;
uint32_t access_flags_;
uint64_t direct_methods_;
uint64_t ifields_;
uint64_t sfields_;
uint64_t virtual_methods_;
uint32_t class_size_;
pid_t clinit_thread_id_;
int32_t dex_class_def_idx_;
int32_t dex_type_idx_;
uint32_t num_direct_methods_;
uint32_t num_instance_fields_;
uint32_t num_reference_instance_fields_;
uint32_t num_reference_static_fields_;
uint32_t num_static_fields_;
uint32_t num_virtual_methods_;
uint32_t object_size_;
uint32_t primitive_type_;
uint32_t reference_instance_offsets_;
uint32_t status_;
static uint32_t java_lang_Class_;
};
class ArtField {
public:
uint32_t declaring_class_;
uint32_t access_flags_;
uint32_t field_dex_idx_;
uint32_t offset_;
};
class ArtMethod {
public:
uint32_t declaring_class_;
uint32_t dex_cache_resolved_methods_;
uint32_t dex_cache_resolved_types_;
uint32_t access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint32_t method_index_;
struct PtrSizedFields {
void* entry_point_from_interpreter_;
void* entry_point_from_jni_;
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
};
}
}
native层逻辑实现 native-lib.cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "art_method.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_jni_fixdemo_DexManager_replace(JNIEnv *env, jclass type, jobject errorMethod,
jobject fixMethod) {
//取ArtMethod结构体
//这里需要将声明ArtMethod的头文件导入
//如果直接将Android中源码的art_method.h导入会牵扯太多文件,这里仅导入了部分声明
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(errorMethod);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(fixMethod);
reinterpret_cast(dmeth->declaring_class_)->class_loader_ =
reinterpret_cast(smeth->declaring_class_)->class_loader_; //for plugin classloader
reinterpret_cast(dmeth->declaring_class_)->clinit_thread_id_ =
reinterpret_cast(smeth->declaring_class_)->clinit_thread_id_;
reinterpret_cast(dmeth->declaring_class_)->status_ = reinterpret_cast(smeth->declaring_class_)->status_-1;
//for reflection invoke
reinterpret_cast(dmeth->declaring_class_)->super_class_ = 0;
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->method_index_ = dmeth->method_index_;
smeth->ptr_sized_fields_.entry_point_from_interpreter_ =
dmeth->ptr_sized_fields_.entry_point_from_interpreter_;
smeth->ptr_sized_fields_.entry_point_from_jni_ =
dmeth->ptr_sized_fields_.entry_point_from_jni_;
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
}
完整demo请移步github Andfix基础功能实现
Andfix兼容
Andfix存在版本兼容问题,已停止更新,后续Sopfix未开源,是收费项目(5000用户以内免费),下文会分析Andfix兼容实现
Andfix的实现方式决定其在兼容性上存在很大的问题,Andfix兼容实现,Android_Andfix兼容和Sophix简单分析