AndFix使用范围
修复紧急或者比较小的bug。
AndFix最大优势:及时生效,不需要重启
及时生效的原因
通过native调用:
未下载修复包加载一次
下载修复包后加载一次,下载完成后调用
缺点
稳定性较差,会受到国内ROM厂商对ArtMethod结构更改的影响,如果要适配的话,是很麻烦的。
实现步骤
1.在要修改的方法上添加注解并生成补丁包(.apatch),其实就是一个dex文件。
2.获取补丁包中的补丁类并遍历其中的方法获取待注解的方法
3.使用补丁中的方法替换bug中的方法
AndFix实现原理
需要把java当做一个xml,不然很难理解AndFix的实现原理,首先需要了解java方法里面崩溃和java方法怎样被修复。
先来看看java方法到底如何执行的,Android是怎样加载java类的(java方法在虚拟机是怎么执行的)?
Android程序执行的第一个类是 ZygoteInit.java,当这个类加载的时候,虚拟机开启一个Zygote进程,为Zygote进程分配一个内存空间,会分配5大区,PC计数器,本地方法栈,方法区,堆区,栈区,主要是后3个,当用户点击了APP,Zygote会以命令行的方式启动一个APP,APP最先加载到内存的类是Application.java,系统如果加载,也会加载Application.class,通过的是ClassLoader来加载的,当一个类被加载到方法区时,会在方法区开辟一个方法表,方法表的大小是由类中的方法多少来决定的,可以把方发表理解成一个数组。new的时候在方法区加载方发表的时候同时会在堆区开辟空间,堆区存放的是类的成员变量(如存放的application1),所有进程都是Zygote进程来孵化的。
先来写一段伪代码:
Application application = new Application();
application.onCreate();
当声明一个类的时候,如
Test test;
这时不会把这个Test加载到内存中,只会在方法区定义一个符号变量叫Test
int(Test 符号变量)。
打断点的时候会看到对象中有 kclass,kclass就是堆中开辟的空间指向的符号变量,同时这个符号变量指向了方法表,当onCreate()方法被调用的时候,由堆区的对象发送一个事件给符号变量,符号变量一看是调用某个方法,此时会将方发表中的onCreate结构体(onCreate是个方法,其实它是个结构体)中对应的字节码进行压栈(压栈是在栈区执行的操作),栈的特点是先进后出,压成一个栈帧,转变成汇编语言。
onCreate结构体
ArtMethod{
方法入口
字节码地址
}
那什么时候才会加载这个Test类呢?
只有在new的时候或者反射的时候才会加载类到内存。
new Test()
了解了这些后,然后需要了解方法存放在 .class中,而.class 存放在dex中
安卓虚拟机加载的dex文件,所以实际开发中热修复是从网络下载一个dex文件来操作。
Java中的Method和虚拟机的ArtMethod是一一对应的,可以通过java中的Method找到虚拟机中的ArtMethod。
art_method.h文件
#include <stdint.h>
namespace art{
namespace mirror{
class Object{
uint32_t klass_;
uint32_t monitor_;
};
class ArtMethod:public Object{
public:
uint32_t access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint32_t method_index_;
uint32_t dex_cache_resolved_methods_;
uint32_t dex_cache_resolved_types_;
uint32_t declaring_class_;
};
}
}
native-lib.cpp文件
#include <jni.h>
#include <string>
#include "art_method.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_andfix_DexManager_replace(JNIEnv *env, jobject instance, jobject wrongMethod,
jobject rightMethod) {
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->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_;
}
上面这段代码在5.0,6.0是没有问题的,如果在其它的版本运行可能会有问题。
Java中的Method和虚拟机的ArtMethod是一一对应的,可以通过java中的Method找到虚拟机中的ArtMethod。
我们是如何加载一个类的?
java中是通过 new 反射 classload 来加载类的。new 反射 classload最终都是通过JNI中的FindClass来加载类的。
native是通多FindClass来加载类的,FindCalss最终会进入class_linker.cc(6.0源码)这个类。
FindClass这个类的作用:
1.检查一个是否正确
2.检查并做加载类的准备工作
cliss_linker.cc中有个DefineClass,DefineClass定义一个空的类,相当于画板的作用(JavaBean),java中的每个类在虚拟机中都对应一个相应的结构体,然后通过newHandler的方式来创建kclass。通过LoadClass来加载一个类,此时Class还在dex中,从dex中加载class,所以需要将Class的信息给到空的类来构建成员变量表(ArtField)和方法表(ArtMethod),其中对成员变量和方法做了判断,不为0的情况下赋值给kclass。
java方法通过虚拟机加载到内存中,虚拟机给每个方法分配的内存字节数是固定的,字节数是根据当前的手机型号或者是Android版本不同而不同。
虚拟机分为Dalvik虚拟机和Art虚拟机:
Dalvik使用的jit(即时编译技术),虚拟机会加载libdalvik.so这个库。
Art使用的是AOT预编译技术,4.4后出现了Art,虚拟机会加载libart.so这个库
Dalvik虚拟机模仿的是Java虚拟机(JVM),它通过libdalvik.so来进行类的加载。
dex到class经历4个阶段 :
构建-初始过程-赋值阶段-初始化完成阶段
如果初始化没有完成,这个方法是不能调用的。
art
字节码二进制文件 ——>本地机器执行指令,所以速度很快
需要注意的是,art和dalvik虚拟机加载类的方式不同,art中使用的是kclass,而dalvik使用的是ClassObject。
因为我们知道,加载.so文件是需要相关的依赖库的,那如果没有相关的依赖库时,虚拟机时如何加载.so文件的呢?
通过一个小例子来说明对虚拟机的hook:
Cat.c文件
int add(int a,int b){
return (a*b);
}
main.c文件
#include<stdio.h>
#include<stdlib.h>
#include<dlfcn.h>
typedef int (*ADD)(int,int);
int main(){
void *handle=dlopen("./libdavik.so",RTLD_LAZY);
ADD add=NULL;
*(void **)(&add)=dlsym(handle,"add");
int reslut=add(2,5);
printf("%d\n",reslut);
return 0;
}
通过执行命令:
root@iZbp15ohd7pim2jay3vbwyZ:~# gcc -fPIC -shared Cat.c -o libdavik.so
root@iZbp15ohd7pim2jay3vbwyZ:~# ls
Cat.c libdavik.so main.c
root@iZbp15ohd7pim2jay3vbwyZ:~# gcc -o main main.c -ldl
root@iZbp15ohd7pim2jay3vbwyZ:~# ls
Cat.c libdavik.so main main.c
root@iZbp15ohd7pim2jay3vbwyZ:~# ./main
10
可以看到,通过将 Cat.c 编译为 libdavik.so,然后将main.c编译为可执行程序,main可以看作我们的虚拟机,让它来加载libdavik.so这个库,通过的是如下这个重要的方法:
void *handle=dlopen("./libdavik.so",RTLD_LAZY);
总结:
1.每一个java方法的大小是固定的
2.方法标中的方法与方法之间是紧密联合的(可以理解为一块连续的内存)
下面来看看阿里的AndFix如何使用
在app下的build.gradle添加依赖库:
implementation 'com.alipay.euler:andfix:0.4.0@aar'
创建一个Caclutor.java,来演示异常以及修复后的情况
public class Caclutor {
public void test(Context context){
// throw new RuntimeException("出异常了");
Toast.makeText(context,"修复了",Toast.LENGTH_SHORT).show();
}
}
然后再MainActivity.java中来操作AndFix的相关API
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void test(View view) {
Caclutor caclutor = new Caclutor();
caclutor.test(this);
}
public void fix(View view) {
PatchManager patchManager = new PatchManager(this);
try{
/**
* 这段代码应该写在application中
*/
String versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
//初始化
patchManager.init(versionName);
/**
* loadPatch() 从网络上下载修复包放到AndFix的私有目录中,然后去加载所有已经存在的修复包
*/
patchManager.loadPatch();
File file = new File(Environment.getExternalStorageDirectory(),"out.apatch");
patchManager.addPatch(file.getAbsolutePath());
}catch (Exception e){
e.printStackTrace();
}
}
}
分别打新旧两个包,old.apk和new.apk,针对Caclutor修改前后。
从git上下载阿里的开源库 https://github.com/alibaba/AndFix
找到tools文件夹,在windows下使用 apkpatch.bat这个工具,在Linux下使用apkpatch.sh这个工具。
old.apk和new.apk是我打包后的两个apk,放到里面的。
命令行定位到tools文件夹,需要使用apkpatch.bat(我的系统是windows系统),执行命令:
apkpatch.bat -f new.apk -t old.apk -o output -k key.jks -p 123456 -a test -e 123456
参数说明
apkpatch.bat -f 新apk -t 旧apk -o 输出目录 -k app签名文件 -p 签名文件密码 -a 签名文件别名 -e 别名密码
-f <new.apk> :新apk
-t <old.apk> :旧apk
-o <output> :输出目录(补丁文件的存放目录)
-k <keystore>: 打包所用的keystore
-p <password>: keystore的密码
-a <alias>: keystore 用户别名
-e <alias password>: keystore 用户别名密码
命令完成后会在当前目录下生成一个output文件夹说明成功了
output文件夹里面有一个名字很长后缀以apttch结尾的文件就是我们所需要的文件,给名为 out.apatch放到外部储存中。
难道这个apatch文件就是我们所需要的吗,前面不是说安卓虚拟机加载的是dex文件吗?
其实apatch是阿里的命名规则,它的本质就是个压缩包,我们把apatch改为zip,然后打开
看到没有,这个 classes.dex 就是我们所需要的虚拟机要加载的修复后的文件,所以不要被这个apatch文件所迷惑了。
那AndFix是如何知道某个方法是需要修复的呢,其实这个很简单,就是将一行一行的代码进行比较,然后把改变的方法加上注解
package com.example.myapplication;
import android.content.Context;
public class Caclutor {
@Replace(clazz = "com.example.myapplication2.Caclutor",method = "test")//注意:这里com.example.myapplication2.Caclutor这个全类名,和本类的不一样,这个包是修复后的Caclutor
public void test(Context context) {
//throw new RuntimeException("出异常了");
}
}
Replace.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Replace {
String clazz();
String method();
}
public void load(File file) {
try {
DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
new File(context.getCacheDir(), "opt").getAbsolutePath(),
Context.MODE_PRIVATE);
Enumeration<String> entry= dexFile.entries();
while (entry.hasMoreElements()) {
// 全类名
String className = entry.nextElement();
Class realClazz=dexFile.loadClass(className, context.getClassLoader());
if (realClazz != null) {
fixClass(realClazz);
}
// Class.forName(className);//forName
}
} catch (Exception e) {
e.printStackTrace();
}
}
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中实现
extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_andfix_DexManager_replace(JNIEnv *env, jobject instance, 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_;
}
上面这个是art虚拟机下的,而如果是dalvik虚拟机则不是所示,并且5.0之上的各个版本的ArtMethod这个结构体都会不同,所以适配起来很麻烦,这也是AndFix的一个缺点。