在此之前已经总结过ClassLoader的原理,以及通过ClassLoader方式实现的热修复思路,实现热修复的方法有很多,大致有三种方式:
- ClassLoader
- Instant Run(ASM字节码插装)
- 底层替换方案
本文重点介绍后两种实现热修复的方式, 第一种方式可以参考这篇文章:ClassLoader&双亲委派模型
Instant Run方案(ASM字节码插装)
关于Instant Run的了解可以参考这篇文章: Android Studio新功能解析,你真的了解Instant Run吗?
Instant Run的原理是在第一次构建apk的时候使用ASM
在每一个方法中
注入一段代码,这段代码的作用是判断这个方法是否发生改变,如果没有改变就什么都不做,如果发生了改变,就会生成一个替换类代替
ASM:
ASM 是一个java字节码修改框架,可以动态的生成类或者修改现有类的功能,可以直接创建class字节码或者在虚拟机执行之前改变现有类的行为;
插桩的作用:
代码插入和代码替换,常见的使用场景有热修复,埋点,性能检测
ASM框架的依赖:
// ASM 相关依赖
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
回忆一下安卓打包流程,其中有一个步骤就是将class字节码通过dx工具转为dex文件,在转化前,我们可以通过ASM对字节码进行修改;
使用ASM进行插桩之前需要了解的知识点:
-
Transform API:
android在将class转成dex之前给我们预留的一个接口 - Gradle自定义插件
整体思路
使用Transform API在字节码转为dex前通过ASM提供的API对字节码进行修改
ASM框架的核心API介绍:
- ClassReader:字节码读取类
- Visitor:调用ClassReader的accept()需要传入Visitor,常见的Visitor有ClassVisitor,MethodVisitor,当类或者方法被读取到时会触发Visitor中对应的事件;
void asm() throws Exception {
FileInputStream fileInputStream = new FileInputStream(new File("字节码class"));
// 字节码分析器
ClassReader clazzReader = new ClassReader(fileInputStream);
// 分析字节码
clazzReader.accept(new MyVisitor(Opcodes.ASM7),ClassReader.EXPAND_FRAMES);
}
创建一个ClassReader并且调用它的accept(),传入一个visitor(ClassVisitor),用于监听字节码的读取;
MyVisitor
static class MyVisitor extends ClassVisitor{
public MyVisitor(int api) {
super(api);
}
// 当读取到类的方法时,会回调这个方法
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(api,methodVisitor,access,name,descriptor);
}
}
在我们自定义的ClassVisitor类中的visitMethod()
,当字节码中Method相关的字节码被读取的时候会回调这个方法,在这个方法中,需要返回一个MethodVisitor
对象,我们可以自定义一个MethodVisitor来对Method相关的字节码进行修改:
MyMethodVisitor:
static class MyMethodVisitor extends AdviceAdapter { // AdviceAdapter是MethodVisitor的子类
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
}
}
AdviceAdapter
是MethodVisitor的子类,它提供了更多的API让我们对字节码进行修改,在此,重写了两个方法,从名称就可以看到这个方法的作用:在这个Method执行前和执行后调用;
在onMethodEnter/Exit中添加插桩的操作:这里的所有操作都是通过ASM提供的方法进行操作,这些方法是跟字节码指令一一对应的,所以需要参照字节码指令来写,可以通过AS的插件ASM Bytecode Viewer查看字节码
1.我需要插入这样的代码:
System.out.println("Hello");
2.通过ASM Bytecode Viewer查看到的字节码:
L0
LINENUMBER 12 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Hello"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 13 L1
RETURN
L2
LOCALVARIABLE this Lcom/leap/latte/asm/Hello; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
3. 在onMethodEnter()插桩的写法:
@Override
protected void onMethodEnter() {
super.onMethodEnter();
getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io/PrintStream;"));
visitLdcInsn("Hello");
invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V"));
}
首先来看字节码,再看插桩写法:
- GETSTATIC指令:获取一个类的静态成员
—对应—
getStatic(),需要传入这个类的Type,这个static成员的名称,这个成员的type(这里需要使用签名类型) - LDC指令:向栈帧中压入Hello字符串
- INVOKEVIRTUAL指令:执行这个对象的方法
—对应—
invokeVirtual() ,需要传入这个对象的类的Type,执行方法的Method(这是asm包下的method,不是反射包下的)
每一个java代码再编译后,都会得到多行字节码,这些字节码描述了底层栈帧操作的行为,在使用asm进行插桩的时候,asm对字节码操作栈帧的步骤一一的进行了封装,所以我们可以通过asm对字节码进行修改;
上面的例子只做简单的参考,具体asm相关的api可以参考
ASM官网
配合trainsform接口在dx转换前修改class字节码
底层替换方案:
底层替换方案会直接在native层修改原有的类,每一个java方法都会在native层有一个对应的ArtMethod指针,这个指针包含了Java方法的所有信息:方法执行的入口,访问权限,执行地址等;
ArtMethod:
修改ArtMethod结构体的某个字段或者替换整个结构体,这就是底层替换方案,采用底层方案主要是阿里系:AndFix,Dexposed,阿里百川;