市面上很多热修复框架,相信大家也都有对比使用过,其他热修复框架这里也不列举对比了,今天就从阿里的AndFix热修复框架入手,学习下AndFix里面的热修复是如何做的。
源码地址:https://github.com/alibaba/AndFix
上次更新已经是三四年前的了,基本上没有维护了,不过还是有学习的价值。 接下来我们就来简单看下里面到底怎么做的,然后自己实现一个热修复。
AndFix实现原理
没错,实现原理就一张图,也可以用一句话来概括,就是方法的替换。 将我们修复过后的方法,把原来的错误方法替换调。那么下次调用的时候就是调用的我们修复过后的方法,这样程序就不会发生问题了。(这里只是很简单的概括了一下,具体大家可以看看AndFix GitHub里面的介绍更为详细)
预习知识:关于JVM虚拟机内存结构
参考资料
1.https://www.cnblogs.com/ityouknow/p/5610232.html
2.https://www.jianshu.com/p/8ba5b90a5c66
3.more。。。。
##1.开始
既然我们已经知道了AndFix是通过方法的替换,那么我们就来梳理下,这个方法应该如何替换?
1.通过查找资料得知,方法无法通过java代码直接实现替换,只能通过NDK C++的方式替换。(涉及到jvm虚拟机的相关知识,方法区等)
2.通过文档中得知,修复要先打包一个修复包,然后进行修复。
好的,那么我们知道了上面2个条件,我们就开始我们的实现之旅。
##2.步骤说明
1.修复有问题的代码,然后编译成Class文件,找出修复好的Class文件
2.将CLass文件打包成Dex文件。
3.将dex放入手机中
4.加载dex文件
5.加载Dex文件中的class文件
6.取出我们需要替换的正确的方法
7.替换方法
###2.1 修复有问题的Class文件
首先我们创建一个有问题的代码如下
public class Calculation {
public Calculation() {
}
public int calculation() {
throw new RuntimeException("手动抛出异常");
}
}
然后我们调用上面的代码如下
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Calculation calculation = new Calculation();
int calculation1 = calculation.calculation();
tv.setText("--->" + calculation1);
}
});
这里通过点击一个按钮执行这个类里面的这个方法,我们看看运行效果
calculation
上图可以看到页面的整个布局,点击左边的按钮,调用了面写的‘calculation’方法,抛出了一个异常。
现在异常代码我们已经造好了,现在我们来修复他。我们这里另外建一个类来修复它。
package com.xiaxiayige.andfixdemo.fix;
import com.xiaxiayige.andfixdemo.Replace;
public class Calculation {
public Calculation() {
}
@Replace(className = "com.xiaxiayige.andfixdemo.Calculation", methodName = "calculation")
public int calculation() {
return 100;
}
}
上面这个类是我在fix包下面复制原来的类,然后修改calculation方法,让他返回一个数值100。 去掉了之前手动抛出的异常。
这里有一点需要注意的是@Replace注解,可能有的人问为什么要加这个呢,原来的方法上并没有这个啊。 这个就需要大家看到上面提到的需要我们找到正确的方法。 这里看看Replace注解类
@Retention(RetentionPolicy.RUNTIME) //运行时注解
@Target(ElementType.METHOD) // 应用在方法之上
public @interface Replace {
//需要替换的类路径名称
String className();
//需要替换的正确的方法名
String methodName();
}
上面的注释已经很好的说明了一切。 我们定义在方法上的注解的原因就是我们加载我们修复包的时候,我们遍历我们修复包中的class文件,取出里面所有的方法,然后查看我们要把带有注解的方法替换给哪个类谁的哪个方法。所以上面修复类的类名和方法名不需要和有问题的类名方法名一致也可以。
2.2打包修复包的Dex文件。(可以运用其他手段打出差分包 bsdiff)
步骤1.我们先build一下项目,然后会生成所有类的class文件。 然后找到我们修复的那个class文件。具体步骤如下
1.点下运行旁边的小锤子即可或者(build->Make Project)
2.然后切换项目视图 如下图1所示
3.找到右键,打开到文件目录,删除其他class文件,只保留修复包的class文件(图2)
4.然后执行一条dx命令,这个dx.bat文件是我们AndroidSDK自带的一个工具,我们可以在你的sdk目录下 sdk\build-tools\29.0.1\dx.bat
5.执行命令 dx --dex --output fix.dex 修复包路径 (见图3),这样我们就可以打包一个dex文件了。
图1
图2
图3
注意(这里我能直接使用dx命令 是因为我把dx.bat 添加到环境变量了)
继续
2.3 把Dex文件push到手机sd卡中。
太简单了,无图演示
2.4 /2.5/2.6 加载dex文件,取出错误的方法和正确的方法
这里我们假设你已经把dex文件push到手机中了,这里就开始我们加载dex,然后取出里面的class文件
private void fix() {
//1.得到dex文件 记得自己去申请权限请求 记得自己去申请权限请求 记得自己去申请权限请求
File dexFilePath = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "fix.dex");
try {
//加载dex文件
DexFile dexFile = DexFile.loadDex(dexFilePath.getAbsolutePath(), new File(getCacheDir(), "opt").getAbsolutePath(), Context.MODE_PRIVATE);
Enumeration<String> entries = dexFile.entries();
while (entries.hasMoreElements()) {
String element = entries.nextElement();
//获取到Class
Class aClass = dexFile.loadClass(element, getClassLoader());
//获取到class中的所有方法
Method[] declaredMethods = aClass.getMethods();
for (Method dest : declaredMethods) {
//获取带有指定注解的方法
Replace annotation = dest.getAnnotation(Replace.class);
if (annotation == null) {
continue;
}
//获取要替换方法的类
String className = annotation.className();
//获取要替换类方法的方法
String methodName = annotation.methodName();
try {
//原始类 含有错误的方法的类。
Class errorClass = Class.forName(className);
Class<?>[] parameterTypes = dest.getParameterTypes();
System.out.println("parameterTypes = " + parameterTypes);
Method srcMethod = errorClass.getDeclaredMethod(methodName, parameterTypes);
System.out.println("errorMethod = " + srcMethod);
// 执行native方法
replaceMethod(srcMethod, dest);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
这里定义了一个native方法
//正确的
public native void replaceMethod(Method errorMethod, Method rightMethod);
然后我们再去写C++代码实现方法的替换
首先我们要拷贝一个art_method.h的头文件到我们的项目中,这个文件在哪儿呢 ,大家可以在Android源码中找到。这里我们通过
http://androidxref.com/ 然后去找,具体的路径在 源码路径/art/runtime/art_method.h ,然后拷贝下来后需要做一些处理,删除我们不需要的东西,这里就很重要了,哪些东西删,哪些不删呢。 这里经过测试,大家只保留里面定义的一些属性就行了,其他一些方法类全部删掉。 (可以参考AndFix项目 哈哈),这里贴一份我的
基于Android 7.0 的ArtMethod,删减之后的文件
namespace art {
union JValue;
class OatQuickMethodHeader;
class ProfilingInfo;
class ScopedObjectAccessAlreadyRunnable;
class StringPiece;
class ShadowFrame;
namespace mirror {
class Array;
class Class;
class IfTable;
class PointerArray;
} // namespace mirror
class ArtMethod {
public:
uint32_t access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint16_t method_index_;
uint16_t hotness_count_;
uint16_t method_dex_index_;
const void *native_method_;
const uint16_t *vmap_table_;
uint32_t dex_cache_resolved_methods_;
uint32_t dex_cache_resolved_types_;
uint32_t declaring_class_;
};
}
这里主要保证的是ArtMethod这里面的一些变量是不能少的,比如 access_flags_ method_dex_index_v这样一些和方法有关系的变量。
然后我们来编写替换方法的代码了。
3.方法替换
extern "C"
JNIEXPORT void JNICALL
Java_com_xiaxiayige_andfixdemo_MainActivity_replaceMethod(JNIEnv *env, jobject instance,
jobject srcMethod,
jobject destMethods) {
//获取到方法
art::ArtMethod *wrongMethod =(art::ArtMethod *) env->FromReflectedMethod(srcMethod);
art::ArtMethod *rightMethod =(art::ArtMethod *) env->FromReflectedMethod(destMethods);
//把正确的方法属性变量赋值给错误的
wrongMethod->access_flags_=rightMethod->access_flags_;
wrongMethod->dex_code_item_offset_=rightMethod->dex_code_item_offset_;
wrongMethod->dex_method_index_=rightMethod->dex_method_index_;
wrongMethod->method_index_=rightMethod->method_index_;
wrongMethod->hotness_count_=rightMethod->hotness_count_;
}
记得文件前面需要引用#include "art_method.h"头文件 ,不然会找不到ArtMethod类。
然后我们就可以跑一下代码看下情况了
Good,程序已经被修复了,正常运行。
再从头到尾梳理一遍把。
修复错误方法--->打包dex文件---->解析dex文件----->找出错误的类,取出错误的方法------>通过ArtMethod实现方法的替换(其实是内存地址的一系列替换,将错误的方法的地址指向正确的方法的地址)
4 最后
我们来看看AndFix是怎么做的。
可以看到最后也是实现的是将新方法的一些数据替换给老的。
5 缺点
我这里是基于Android7.0来做的,所以在其他手机上运行会有问题,可以看到的是AndFix也是定义了各种版本的c文件,用来兼容不同手机的方法替换。 所以每新出一个系统我们就要去做一次兼容还是挺麻烦的。
这里AndFix之支持到了7.0的系统,我们也可以根据此思路,去兼容8.0,9.0的系统提供给自己使用。
。
源码地址:AndFixDemo
多想想别人怎么实现,实现思路是什么,学会阅读源码是一个好习惯。