在 《深刻探索编译插桩技术(2、AspectJ)》 一文中咱们深刻学习了 AspectJ 在 Android 下的使用。能够看到 AspectJ 很是强大,可是它也只能实现 50% 的字节码操做场景,若是想要实现 100% 的字节码操做场景,那么就不得不使用 ASM。html
此外,AspectJ 有着一系列弊端: 因为其基于规则,因此其切入点相对固定,对于字节码文件的操做自由度以及开发的掌控度就大打折扣。而且,他会额外生成一些包装代码,对性能以及包大小有必定影响。java
而 ASM 基本上能够实现任何对字节码的操做,也就是自由度和开发的掌控度很高。它提供了 访问者模式来访问字节码文件,而且只注入咱们想要注入的代码。android
ASM 最初起源于一个博士的研究项目,在 2002 年开源,并从 5.x 版本便开始支持 Java 8。而且,ASM 是诸多 JVM 语言钦定的字节码生成库,它在效率与性能方面的优点要远超其它的字节码操做库如 javassist、AspectJ。git
使用 ASM 操做字节码的优点与逆势都 比较明显,其分别以下所示。github
上手难度较大,须要对 Java 字节码有比较充分的了解。web
对于 ASM 而言,它提供了 两种模型:对象模型和事件模型。算法
下面,咱们就先来说讲 ASM 的对象模型。编程
对象模型的 本质 是一个 被封装事后的事件模型,它 使用了树状图的形式来描述一个类,其中包含多个节点,例如方法节点、字段节点等等,而每一个节点又有子节点,例如方法节中有操做码子节点 等等。下面咱们先来了解下由这种树状图模式实现的对象模型的利弊。json
在对象模型下的 ASM 有 两类操做纬度,分别以下所示:bootstrap
获取节点
:获取指定类、字段、方法节点。操控操做码(针对方法节点)
:获取操做码位置、替换、删除、插入操做码、输出字节码。下面咱们就来分别来了解下 ASM 的这两类操做。
获取一个类节点的代码以下所示:
ClassNode classNode = new ClassNode();
// 1
ClassReader classReader = new ClassReader(bytes);
// 2
classReader.accept(classNode, 0);
复制代码
在注释1处,将字节数组传入一个新建立的 ClassReader,这时 ASM 会使用 ClassReader 来解析字节码。接着,在注释2处,ClassReader 在解析完字节码以后即可以经过 accept 方法来将结果写入到一个 ClassNode 对象之中。
那么,一个 ClassNode 具体又包含哪些信息呢?
以下所示:
类型 | 名称 | 说明 |
---|---|---|
int | version | class文件的major版本(编译的java版本) |
int | access | 访问级 |
String | name | 类名,采用全地址,如java/lang/String |
String | signature | 签名,一般是null |
String | superName | 父类类名,采用全地址 |
List | interfaces | 实现的接口,采用全地址 |
String | sourceFile | 源文件,可能为null |
String | sourceDebug | debug源,可能为null |
String | outerClass | 外部类 |
String | outerMethod | 外部方法 |
String | outerMethodDesc | 外部方法描述(包括方法参数和返回值) |
List | visibleAnnotations | 可见的注解 |
List | invisibleAnnotations | 不可见的注解 |
List | attrs | 类的Attribute |
List | innerClasses | 类的内部类列表 |
List | fields | 类的字段列表 |
List | methods | 类的方法列表 |
获取一个字段节点的代码以下所示:
for(FieldNode fieldNode : (List)classNode.fields) {
// 1
if(fieldNode.name.equals("password")) {
// 2
fieldNode.access = Opcodes.ACC_PUBLIC;
}
}
复制代码
字段节点列表 fields 是一个 ArrayList,它储存着类节点的全部字段。在注释1处,咱们经过遍历 fields 集合的方式来找到目标字段节点。接着,在注释2处,咱们将目标字段节点的访问权限置为 public。
除此以外,咱们还能够为类添加须要的字段,代码以下所示:
FieldNode fieldNode = new FieldNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "JsonChao", "I", null, null);
classNode.fields.add(fieldNode);
复制代码
在上述代码中,咱们直接给目标类节点添加了一个 "public static int JsonChao" 的字段,须要注意的是,第三个参数的 "I" 表示的是 int 的类型描述符。
那么,对于一个字段节点,又包含有哪些字段信息呢?
以下所示:
类型 | 名称 | 说明 |
---|---|---|
int | access | 访问级 |
String | name | 字段名 |
String | signature | 签名,一般是 null |
String | desc | 类型描述,例如 Ljava/lang/String、D(double)、F(float) |
Object | value | 初始值,一般为 null |
List | visibleAnnotations | 可见的注解 |
List | invisibleAnnotations | 不可见的注解 |
List | attrs | 字段的 Attribute |
接下来,咱们看看如何获取一个方法节点。
获取指定的方法节点的代码以下所示:
for(MethodNode methodNode : (List)classNode.methods) {
// 一、判断方法名是否匹配目标方法
if(methodNode.name.equals("getName")) {
// 二、进行操做
}
}
复制代码
methods 同 fields 同样,也是一个 ArrayList,经过遍历并判断方法名的方式便可匹配到目标方法。
对于一个方法节点来讲,它包含有以下信息:
类型 | 名称 | 说明 |
---|---|---|
int | access | 访问级 |
String | name | 方法名 |
String | desc | 方法描述,其包含方法的返回值和参数 |
String | signature | 签名,一般是null |
List | exceptions | 可能返回的异常列表 |
List | visibleAnnotations | 可见的注解列表 |
List | invisibleAnnotations | 不可见的注解列表 |
List | attrs | 方法的Attribute列表 |
Object | annotationDefault | 默认的注解 |
List[] | visibleParameterAnnotations | 可见的参数注解列表 |
List[] | invisibleParameterAnnotations | 不可见的参数注解列表 |
InsnList | instructions | 操做码列表 |
List | tryCatchBlocks | try-catch块列表 |
int | maxStack | 最大操做栈的深度 |
int | maxLocals | 最大局部变量区的大小 |
List | localVariables | 本地(局部)变量节点列表 |
在操控字节码以前,咱们必须先了解下 instructions
,即 操做码列表,它是 方法节点中用于存储操做码的地方,其中 每个元素都表明一行操做码。
ASM 将一行字节码封装为一个 xxxInsnNode(Insn 表示的是 Instruction 的缩写,即指令/操做码),例如 ALOAD/ARestore 指令被封装入变量操做码节点 VarInsnNode,INVOKEVIRTUAL 指令则会被封入方法操做码节点 MethodInsnNode 之中。
对于全部的指令节点 xxxInsnNode 来讲,它们都继承自抽象操做码节点 AbstractInsnNode
。其全部的派生类使用详情以下所示。
名称 | 说明 | 参数 |
---|---|---|
FieldInsnNode | 用于 GETFIELD 和 PUTFIELD 之类的字段操做的字节码 | String owner 字段所在的类 String name 字段的名称 String desc 字段的类型 |
FrameNode | 栈映射帧的对应的帧节点 | 待补充 |
IincInsnNode | 用于 IINC 变量自加操做的字节码 | int var:目标局部变量的位置 int incr: 要增长的数 |
InsnNode | 一切无参数值操做的字节码,例如 ALOAD_0,DUP(注意不包含 POP) | 无 |
IntInsnNode | 用于 BIPUSH、SIPUSH 和 NEWARRAY 这三个直接操做整数的操做 | int operand:操做的整数值 |
InvokeDynamicInsnNode | 用于 Java7 新增的 INVOKEDYNAMIC 操做的字节码 | String name:方法名称 String desc:方法描述 Handle bsm:句柄 Object[] bsmArgs:参数常量 |
JumpInsnNode | 用于 IFEQ 或 GOTO 等跳转操做字节码 | LabelNode lable:目标 lable |
LabelNode | 一个用于表示跳转点的 Label 节点 | 无 |
LdcInsnNode | 使用 LDC 加载常量池中的引用值并进行插入的字节码 | Object cst:引用值 |
LineNumberNode | 表示行号的节点 | int line:行号 LabelNode start:对应的第一个 Label |
LookupSwitchInsnNode | 用于实现 LOOKUPSWITCH 操做的字节码 | LabelNode dflt:default 块对应的 Lable List keys 键列表 List labels:对应的 Label 节点列表 |
MethodInsnNode | 用于 INVOKEVIRTUAL 等传统方法调用操做的字节码,不适用于 Java7 新增的 INVOKEDYNAMIC | String owner :方法所在的类 String name :方法名称 String desc:方法描述 |
MultiANewArrayInsnNode | 用于 MULTIANEWARRAY 操做的字节码 | String desc:类型描述 int dims:维数 |
TableSwitchInsnNode | 用于实现 TABLESWITCH 操做的字节码 | int min:键的最小值 int max:键的最大值 LabelNode dflt:default 块对应的 Lable List labels:对应的 Label 节点列表 |
TypeInsnNode | 用于实现 NEW、ANEWARRAY 和 CHECKCAST 等类型相关操做的字节码 | String desc:类型 |
VarInsnNode | 用于实现 ALOAD、ASTORE 等局部变量操做的字节码 | int var:局部变量 |
下面,咱们就开始来说解下字节码操控有哪几种常见的方式。
获取指定操做码位置的代码以下所示:
for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {
if(ainNode.getOpcode() == Opcodes.SIPUSH && ((IntInsnNode)ainNode).operand == 16) {
....//进行操做
}
}
复制代码
因为通常状况下咱们都没法肯定操做码在列表中的具体位置,所以 一般会经过遍历的方式去判断其关键特征,以此来定位指定的操做码,上述代码就能定位到一个 SIPUSH 16 的字节码,须要注意的是,有时一个方法中会有多个相同的指令,这是咱们须要靠判断先后字节码识别其特征来定位,也能够记下其命中次数而后设定在某一次进行操做,通常状况下咱们都是使用的第二种。
替换指定的操做码的代码以下所示:
for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {
if(ainNode.getOpcode() == Opcodes.BIPUSH && ((IntInsnNode)ainNode).operand == 16) {
methodNode.instructions.set(ainNode, new VarInsnNode(Opcodes.ALOAD, 1));
}
}
复制代码
这里咱们 直接调用了 InsnList 的 set 方法就能替换指定的操做码对象,咱们在获取了 "BIPUSH 64" 字节码的位置后,便将封装它的操做码替换为一个新的 VarInsnNode 操做码,这个新操做码封装了 "ALOAD 1" 字节码, 将原程序中 将值设为16
替换为 将值设为局部变量1
。
methodNode.instructions.remove(xxx);
复制代码
xxx 表示的是要删除的操做码实例,咱们直接调用用 InsnList 的 remove 方法将它移除掉便可。
InsnList 主要提供了 四类 方法用于插入字节码,以下所示:
add(AbstractInsnNode insn)
: 将一个操做码添加到 InsnList 的末尾。insert(AbstractInsnNode insn)
: 将一个操做码插入到这个 InsnList 的开头。insert(AbstractInsnNode insnNode,AbstractInsnNode insn)
: 将一个操做码插入到另外一个操做码的下面。insertBefore(AbstractInsnNode insnNode,AbstractInsnNode insn)
将一个操做码插入到另外一个操做码的上面接下来看看如何使用这些方法插入指定的操做码,代码以下所示:
for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {
if(ainNode.getOpcode() == Opcodes.BIPUSH && ((IntInsnNode)ainNode).operand == 16) {
methodNode.instructions.insert(ainNode, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/awt/image/BufferedImage", "getWidth", "(Ljava/awt/image/ImageObserver;)I"));
methodNode.instructions.insert(ainNode, new InsnNode(Opcodes.ACONSTNULL));
methodNode.instructions.set(ainNode, new VarInsnNode(Opcodes.ALOAD, 1));
}
}
复制代码
这样,咱们就能将
BIPUSH 16
复制代码
替换为
ALOAD 1
ACONSTNULL
INVOKEVIRTUAL java/awt/image/BufferedImage.getWidth(Ljava/awt/image/ImageObserver;)I
复制代码
当咱们操控完指定的类节点以后,就可使用 ASM 的 ClassWriter 类来输出字节码,代码以下所示:
// 一、让 ClassWriter 自行计算最大栈深度和栈映射帧等信息
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTEFRAMES);
classNode.accept(classWriter);
return classWriter.toByteArray();
复制代码
关于 ClassWriter 的具体用法,咱们会在 ASM Core API 这部分来进行逐步讲解。下面👇,咱们就先来看看 ASM 的事件模型。
对象模型是由事件模型封装而成,所以事件模型的上手难度会更大一些。
对于事件模型来讲,它 采用了设计模式中的访问者模式。它的出现是为了更好地解决这样一种需求:有 A 个元素和 N 种算法,每一个算法都能做用于任意一个元素,而且在不一样的元素上有不一样的运行方式。
在访问者模式出现以前,咱们一般会在每个元素对应的类中添加 N 个方法,而后在每个方法中去实现一个算法,可是,这样的作法容易致使代码耦合性太高,而且可维护性差。
所以,访问者模式应运而生,咱们能够 创建 N 个访问者,而且每个访问者拥有一个算法及其内部的 A 种运行方式。当咱们须要调用一个算法时,就让相应的访问者去访问元素,而后让访问者根据被访问对象选择相应的算法。
须要注意的是,访问者并无直接去操做元素,而是先让元素类调用 accept 方法接收访问者,而后,访问者在元素类的内部方法中开始调用 visit 方法访问当前的元素类。这样,访问者便能直接访问元素类中的内部私有成员,其优点在于 避免了暴露没必要要的内部细节。
要理解 ASM 的事件模型,咱们就须要对其中的 两个重要成员的工做原理 有较深的了解。它们即是 类访问者 ClassVisitor 与 类读取(解析)者 ClassReader。
从字节码的视角中,一个 Java 类由不少组件凝聚而成,而这之中便包括超类、接口、属性、域和方法等等。当咱们在使用 ASM 进行操控时,能够将它们视为一个个与之对应的事件。所以 ASM 提供了一个 类访问者 ClassVisitor,以经过它来访问当前类的各个组件,当解析器 ClassReader 依次遇到上述的各个组件时,ClassVisitor 上对应的 visitor 事件处理器方法均会被一一调用。
与类类似,方法也是由多个组件凝聚而成的,其对应着方法属性、注解及编译后的代码(Class 字节码)。ASM 的 MethodVisitor 提供了一种 hook(钩子)机制,以便可以访问方法中的每个操做码,这样咱们便可以对字节码文件进行细粒度地修改。
下面,咱们便来一一分析下它们。
一般咱们在使用 ASM 的访问者模式有一个模板代码,以下所示:
InputStream is = new FileInputStream(classFile);
// 1
ClassReader classReader = new ClassReader(is);
// 2
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// 3
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
// 4
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
复制代码
首先,在注释1处,咱们 将目标文件转换为流的形式,并将它融入类读取器 ClassReader 之中。而后,在注释2处,咱们 构建了一个类写入器 ClassWriter,其参数 COMPUTE_MAXS 的做用是将自动计算本地变量表最大值和操做数栈最大值的任务托付给了ASM。接着,在注释3处,新建了一个自定义的类访问器,这个自定义的 ClassVisitor
的做用是为了在每个方法的开始和结尾处插入相应的记时代码,以便统计出每个方法的耗时。最后,在注释4处,类读取器 ClassReader 实例这个被访问者调用了自身的 accept 方法接收了一个 classVisitor 实例,须要注意的是,第二个参数指定了 EXPAND_FRAMES
,旨在说明在读取 class 的时候须要同时展开栈映射帧(StackMap Frame),若是咱们须要使用自定义的 MethodVisitor 去修改方法中的指令时必需要指定这个参数,。
上面,咱们说到了栈映射帧(StackMap Frame),它究竟是什么呢?
它是 Java 6 之后引入的一种验证机制,用于 检验 Java 字节码的正确性。它的工做方式是 记录每个关键步骤完成后其方法中操做数栈的理论状态,而后,在实际运行的时候,ASM 会将其实际状态和理论状态对比,若是状态不一致则代表出现了错误。
但栈映射帧的实现并不简单,所以经过调用 classReader 实例的 accept 方法咱们即可以让 ASM 自动去计算栈映射帧,尽管这 会增长 50% 的额外运算。此外,可能会有小几率的状况遇到 栈映射帧验证失败 的状况,例如:VerifyError: Inconsistent stackmap frames at branch target
这个错误。
最多见的缘由可能就是因为 字节码写错形成的,此时,咱们应该去检查对应的字节码实现代码。此外,也多是 JDK 版本的支持问题或是 ASM 自身的缺陷,可是,这种状况几乎不会发生。
如今,让咱们再回到上述注释4处的代码,在这里,咱们调用了 classReader 的 accept 方法接收了一个访问者 classVisitor,下面,咱们来看看其内部的实现,代码以下所示(源码实现较长,这里咱们只需关注注释处的代码便可:
/**
* Makes the given visitor visit the Java class of this {@link ClassReader}
* . This class is the one specified in the constructor (see
* {@link #ClassReader(byte[]) ClassReader}).
*
* @param classVisitor
* the visitor that must visit this class.
* @param flags
* option flags that can be used to modify the default behavior
* of this class. See {@link #SKIP_DEBUG}, {@link #EXPAND_FRAMES}
* , {@link #SKIP_FRAMES}, {@link #SKIP_CODE}.
*/
public void accept(final ClassVisitor classVisitor, final int flags) {
accept(classVisitor, new Attribute[0], flags);
}
复制代码
在 accept 方法中又继续调用了 classReader 的另外一个 accept 重载方法,以下所示:
public void accept(final ClassVisitor classVisitor,
final Attribute[] attrs, final int flags) {
int u = header; // current offset in the class file
char[] c = new char[maxStringLength]; // buffer used to read strings
Context context = new Context();
context.attrs = attrs;
context.flags = flags;
context.buffer = c;
// 一、读取类的描述信息,例如 access、name 等等
int access = readUnsignedShort(u);
String name = readClass(u + 2, c);
String superClass = readClass(u + 4, c);
String[] interfaces = new String[readUnsignedShort(u + 6)];
u += 8;
for (int i = 0; i < interfaces.length; ++i) {
interfaces[i] = readClass(u, c);
u += 2;
}
// 二、读取类的属性信息,例如签名 signature、sourceFile 等等。
String signature = null;
String sourceFile = null;
String sourceDebug = null;
String enclosingOwner = null;
String enclosingName = null;
String enclosingDesc = null;
int anns = 0;
int ianns = 0;
int tanns = 0;
int itanns = 0;
int innerClasses = 0;
Attribute attributes = null;
u = getAttributes();
for (int i = readUnsignedShort(u); i > 0; --i) {
String attrName = readUTF8(u + 2, c);
// tests are sorted in decreasing frequency order
// (based on frequencies observed on typical classes)
if ("SourceFile".equals(attrName)) {
sourceFile = readUTF8(u + 8, c);
} else if ("InnerClasses".equals(attrName)) {
innerClasses = u + 8;
} else if ("EnclosingMethod".equals(attrName)) {
enclosingOwner = readClass(u + 8, c);
int item = readUnsignedShort(u + 10);
if (item != 0) {
enclosingName = readUTF8(items[item], c);
enclosingDesc = readUTF8(items[item] + 2, c);
}
} else if (SIGNATURES && "Signature".equals(attrName)) {
signature = readUTF8(u + 8, c);
} else if (ANNOTATIONS
&& "RuntimeVisibleAnnotations".equals(attrName)) {
anns = u + 8;
} else if (ANNOTATIONS
&& "RuntimeVisibleTypeAnnotations".equals(attrName)) {
tanns = u + 8;
} else if ("Deprecated".equals(attrName)) {
access |= Opcodes.ACC_DEPRECATED;
} else if ("Synthetic".equals(attrName)) {
access |= Opcodes.ACC_SYNTHETIC
| ClassWriter.ACC_SYNTHETIC_ATTRIBUTE;
} else if ("SourceDebugExtension".equals(attrName)) {
int len = readInt(u + 4);
sourceDebug = readUTF(u + 8, len, new char[len]);
} else if (ANNOTATIONS
&& "RuntimeInvisibleAnnotations".equals(attrName)) {
ianns = u + 8;
} else if (ANNOTATIONS
&& "RuntimeInvisibleTypeAnnotations".equals(attrName)) {
itanns = u + 8;
} else if ("BootstrapMethods".equals(attrName)) {
int[] bootstrapMethods = new int[readUnsignedShort(u + 8)];
for (int j = 0, v = u + 10; j < bootstrapMethods.length; j++) {
bootstrapMethods[j] = v;
v += 2 + readUnsignedShort(v + 2) << 1;
}
context.bootstrapMethods = bootstrapMethods;
} else {
Attribute attr = readAttribute(attrs, attrName, u + 8,
readInt(u + 4), c, -1, null);
if (attr != null) {
attr.next = attributes;
attributes = attr;
}
}
u += 6 + readInt(u + 4);
}
// 三、访问类的描述信息
classVisitor.visit(readInt(items[1] - 7), access, name, signature,
superClass, interfaces);
// 四、访问源码和 debug 信息
if ((flags & SKIP_DEBUG) == 0
&& (sourceFile != null || sourceDebug != null)) {
classVisitor.visitSource(sourceFile, sourceDebug);
}
// 五、访问外部类
if (enclosingOwner != null) {
classVisitor.visitOuterClass(enclosingOwner, enclosingName,
enclosingDesc);
}
// 六、访问类注解和类型注解
if (ANNOTATIONS && anns != 0) {
for (int i = readUnsignedShort(anns), v = anns + 2; i > 0; --i) {
v = readAnnotationValues(v + 2, c, true,
classVisitor.visitAnnotation(readUTF8(v, c), true));
}
}
if (ANNOTATIONS && ianns != 0) {
for (int i = readUnsignedShort(ianns), v = ianns + 2; i > 0; --i) {
v = readAnnotationValues(v + 2, c, true,
classVisitor.visitAnnotation(readUTF8(v, c), false));
}
}
if (ANNOTATIONS && tanns != 0) {
for (int i = readUnsignedShort(tanns), v = tanns + 2; i > 0; --i) {
v = readAnnotationTarget(context, v);
v = readAnnotationValues(v + 2, c, true,
classVisitor.visitTypeAnnotation(context.typeRef,
context.typePath, readUTF8(v, c), true));
}
}
if (ANNOTATIONS && itanns != 0) {
for (int i = readUnsignedShort(itanns), v = itanns + 2; i > 0; --i) {
v = readAnnotationTarget(context, v);
v = readAnnotationValues(v + 2, c, true,
classVisitor.visitTypeAnnotation(context.typeRef,
context.typePath, readUTF8(v, c), false));
}
}
// 七、访问类的属性
while (attributes != null) {
Attribute attr = attributes.next;
attributes.next = null;
classVisitor.visitAttribute(attributes);
attributes = attr;
}
// 八、访问内部类
if (innerClasses != 0) {
int v = innerClasses + 2;
for (int i = readUnsignedShort(innerClasses); i > 0; --i) {
classVisitor.visitInnerClass(readClass(v, c),
readClass(v + 2, c), readUTF8(v + 4, c),
readUnsignedShort(v + 6));
v += 8;
}
}
// 九、访问字段和方法
u = header + 10 + 2 * interfaces.length;
for (int i = readUnsignedShort(u - 2); i > 0; --i) {
u = readField(classVisitor, context, u);
}
u += 2;
for (int i = readUnsignedShort(u - 2); i > 0; --i) {
u = readMethod(classVisitor, context, u);
}
// 访问当前类结束时调用
classVisitor.visitEnd();
}
复制代码
首先,在 classReader 实例的 accept 方法中的注释1和注释2处,咱们会 先开始进行类相关的字节码解析的工做:读取了类的描述和属性信息。接着,在注释3 ~ 注释8处,咱们调用了 classVisitor 一系列的 visitxxx 方法访问 classReader 解析完字节码后保存在内存的信息。而后,在注释9处,分别调用了 readField 方法和 readMethod 方法去访问类中的方法和字段。最后,调用 classVisitor 的 visitEnd 标识已访问结束。
这里,咱们先来看看 readField
的源码实现,以下所示:
/**
* Reads a field and makes the given visitor visit it.
*
* @param classVisitor
* the visitor that must visit the field.
* @param context
* information about the class being parsed.
* @param u
* the start offset of the field in the class file.
* @return the offset of the first byte following the field in the class.
*/
private int readField(final ClassVisitor classVisitor,
final Context context, int u) {
// 一、读取字段的描述信息
char[] c = context.buffer;
int access = readUnsignedShort(u);
String name = readUTF8(u + 2, c);
String desc = readUTF8(u + 4, c);
u += 6;
// 二、读取字段的属性
String signature = null;
int anns = 0;
int ianns = 0;
int tanns = 0;
int itanns = 0;
Object value = null;
Attribute attributes = null;
for (int i = readUnsignedShort(u); i > 0; --i) {
String attrName = readUTF8(u + 2, c);
// tests are sorted in decreasing frequency order
// (based on frequencies observed on typical classes)
if ("ConstantValue".equals(attrName)) {
int item = readUnsignedShort(u + 8);
value = item == 0 ? null : readConst(item, c);
} else if (SIGNATURES && "Signature".equals(attrName)) {
signature = readUTF8(u + 8, c);
} else if ("Deprecated".equals(attrName)) {
access |= Opcodes.ACC_DEPRECATED;
} else if ("Synthetic".equals(attrName)) {
access |= Opcodes.ACC_SYNTHETIC
| ClassWriter.ACC_SYNTHETIC_ATTRIBUTE;
} else if (ANNOTATIONS
&& "RuntimeVisibleAnnotations".equals(attrName)) {
anns = u + 8;
} else if (ANNOTATIONS
&& "RuntimeVisibleTypeAnnotations".equals(attrName)) {
tanns = u + 8;
} else if (ANNOTATIONS
&& "RuntimeInvisibleAnnotations".equals(attrName)) {
ianns = u + 8;
} else if (ANNOTATIONS
&& "RuntimeInvisibleTypeAnnotations".equals(attrName)) {
itanns = u + 8;
} else {
Attribute attr = readAttribute(context.attrs, attrName, u + 8,
readInt(u + 4), c, -1, null);
if (attr != null) {
attr.next = attributes;
attributes = attr;
}
}
u += 6 + readInt(u + 4);
}
u += 2;
// 三、访问字段的声明
FieldVisitor fv = classVisitor.visitField(access, name, desc,
signature, value);
if (fv == null) {
return u;
}
// 四、访问字段的注解和类型注解
if (ANNOTATIONS && anns != 0) {
for (int i = readUnsignedShort(anns), v = anns + 2; i > 0; --i) {
v = readAnnotationValues(v + 2, c, true,
fv.visitAnnotation(readUTF8(v, c), true));
}
}
if (ANNOTATIONS && ianns != 0) {
for (int i = readUnsignedShort(ianns), v = ianns + 2; i > 0; --i) {
v = readAnnotationValues(v + 2, c, true,
fv.visitAnnotation(readUTF8(v, c), false));
}
}
if (ANNOTATIONS && tanns != 0) {
for (int i = readUnsignedShort(tanns), v = tanns + 2; i > 0; --i) {
v = readAnnotationTarget(context, v);
v = readAnnotationValues(v + 2, c, true,
fv.visitTypeAnnotation(context.typeRef,
context.typePath, readUTF8(v, c), true));
}
}
if (ANNOTATIONS && itanns != 0) {
for (int i = readUnsignedShort(itanns), v = itanns + 2; i > 0; --i) {
v = readAnnotationTarget(context, v);
v = readAnnotationValues(v + 2, c, true,
fv.visitTypeAnnotation(context.typeRef,
context.typePath, readUTF8(v, c), false));
}
}
// 五、访问字段的属性
while (attributes != null) {
Attribute attr = attributes.next;
attributes.next = null;
fv.visitAttribute(attributes);
attributes = attr;
}
// 访问字段结束时调用
fv.visitEnd();
return u;
}
复制代码
同读取类信息的时候相似,首先,在注释1和注释2处,会 先开始进行字段相关的字节码解析的工做:读取了字段的描述和属性信息。而后,在注释3 ~ 注释5处 按顺序访问了字段的描述、注解、类型注解及其属性信息。最后,调用了 FieldVisitor 实例的 visitEnd 方法结束了字段信息的访问。
下面,咱们在看看 readMethod
的实现代码,以下所示:
/**
* Reads a method and makes the given visitor visit it.
*
* @param classVisitor
* the visitor that must visit the method.
* @param context
* information about the class being parsed.
* @param u
* the start offset of the method in the class file.
* @return the offset of the first byte following the method in the class.
*/
private int readMethod(final ClassVisitor classVisitor,
final Context context, int u) {
// 一、读取方法描述信息
char[] c = context.buffer;
context.access = readUnsignedShort(u);
context.name = readUTF8(u + 2, c);
context.desc = readUTF8(u + 4, c);
u += 6;
// 二、读取方法属性信息
int code = 0;
int exception = 0;
String[] exceptions = null;
String signature = null;
int methodParameters = 0;
int anns = 0;
int ianns = 0;
int tanns = 0;
int itanns = 0;
int dann = 0;
int mpanns = 0;
int impanns = 0;
int firstAttribute = u;
Attribute attributes = null;
for (int i = readUnsignedShort(u); i > 0; --i) {
String attrName = readUTF8(u + 2, c);
// tests are sorted in decreasing frequency order
// (based on frequencies observed on typical classes)
if ("Code".equals(attrName)) {
if ((context.flags & SKIP_CODE) == 0) {
code = u + 8;
}
} else if ("Exceptions".equals(attrName)) {
exceptions = new String[readUnsignedShort(u + 8)];
exception = u + 10;
for (int j = 0; j < exceptions.length; ++j) {
exceptions[j] = readClass(exception, c);
exception += 2;
}
} else if (SIGNATURES && "Signature".equals(attrName)) {
signature = readUTF8(u + 8, c);
} else if ("Deprecated".equals(attrName)) {
context.access |= Opcodes.ACC_DEPRECATED;
} else if (ANNOTATIONS
&& "RuntimeVisibleAnnotations".equals(attrName)) {
anns = u + 8;
} else if (ANNOTATIONS
&& "RuntimeVisibleTypeAnnotations".equals(attrName)) {
tanns = u + 8;
} else if (ANNOTATIONS && "AnnotationDefault".equals(attrName)) {
dann = u + 8;
} else if ("Synthetic".equals(attrName)) {
context.access |= Opcodes.ACC_SYNTHETIC
| ClassWriter.ACC_SYNTHETIC_ATTRIBUTE;
} else if (ANNOTATIONS
&& "RuntimeInvisibleAnnotations".equals(attrName)) {
ianns = u + 8;
} else if (ANNOTATIONS
&& "RuntimeInvisibleTypeAnnotations".equals(attrName)) {
itanns = u + 8;
} else if (ANNOTATIONS
&& "RuntimeVisibleParameterAnnotations".equals(attrName)) {
mpanns = u + 8;
} else if (ANNOTATIONS
&& "RuntimeInvisibleParameterAnnotations".equals(attrName)) {
impanns = u + 8;
} else if ("MethodParameters".equals(attrName)) {
methodParameters = u + 8;
} else {
Attribute attr = readAttribute(context.attrs, attrName, u + 8,
readInt(u + 4), c, -1, null);
if (attr != null) {
attr.next = attributes;
attributes = attr;
}
}
u += 6 + readInt(u + 4);
}
u += 2;
// 三、访问方法描述信息
MethodVisitor mv = classVisitor.visitMethod(context.access,
context.name, context.desc, signature, exceptions);
if (mv == null) {
return u;
}
/*
* if the returned MethodVisitor is in fact a MethodWriter, it means
* there is no method adapter between the reader and the writer. If, in
* addition, the writer's constant pool was copied from this reader
* (mw.cw.cr == this), and the signature and exceptions of the method
* have not been changed, then it is possible to skip all visit events
* and just copy the original code of the method to the writer (the
* access, name and descriptor can have been changed, this is not
* important since they are not copied as is from the reader).
*/
if (WRITER && mv instanceof MethodWriter) {
MethodWriter mw = (MethodWriter) mv;
if (mw.cw.cr == this && signature == mw.signature) {
boolean sameExceptions = false;
if (exceptions == null) {
sameExceptions = mw.exceptionCount == 0;
} else if (exceptions.length == mw.exceptionCount) {
sameExceptions = true;
for (int j = exceptions.length - 1; j >= 0; --j) {
exception -= 2;
if (mw.exceptions[j] != readUnsignedShort(exception)) {
sameExceptions = false;
break;
}
}
}
if (sameExceptions) {
/*
* we do not copy directly the code into MethodWriter to
* save a byte array copy operation. The real copy will be
* done in ClassWriter.toByteArray().
*/
mw.classReaderOffset = firstAttribute;
mw.classReaderLength = u - firstAttribute;
return u;
}
}
}
// 四、访问方法参数信息
if (methodParameters != 0) {
for (int i = b[methodParameters] & 0xFF, v = methodParameters + 1; i > 0; --i, v = v + 4) {
mv.visitParameter(readUTF8(v, c), readUnsignedShort(v + 2));
}
}
// 五、访问方法的注解信息
if (ANNOTATIONS && dann != 0) {
AnnotationVisitor dv = mv.visitAnnotationDefault();
readAnnotationValue(dann, c, null, dv);
if (dv != null) {
dv.visitEnd();
}
}
if (ANNOTATIONS && anns != 0) {
for (int i = readUnsignedShort(anns), v = anns + 2; i > 0; --i) {
v = readAnnotationValues(v + 2, c, true,
mv.visitAnnotation(readUTF8(v, c), true));
}
}
if (ANNOTATIONS && ianns != 0) {
for (int i = readUnsignedShort(ianns), v = ianns + 2; i > 0; --i) {
v = readAnnotationValues(v + 2, c, true,
mv.visitAnnotation(readUTF8(v, c), false));
}
}
if (ANNOTATIONS && tanns != 0) {
for (int i = readUnsignedShort(tanns), v = tanns + 2; i > 0; --i) {
v = readAnnotationTarget(context, v);
v = readAnnotationValues(v + 2, c, true,
mv.visitTypeAnnotation(context.typeRef,
context.typePath, readUTF8(v, c), true));
}
}
if (ANNOTATIONS && itanns != 0) {
for (int i = readUnsignedShort(itanns), v = itanns + 2; i > 0; --i) {
v = readAnnotationTarget(context, v);
v = readAnnotationValues(v + 2, c, true,
mv.visitTypeAnnotation(context.typeRef,
context.typePath, readUTF8(v, c), false));
}
}
if (ANNOTATIONS && mpanns != 0) {
readParameterAnnotations(mv, context, mpanns, true);
}
if (ANNOTATIONS && impanns != 0) {
readParameterAnnotations(mv, context, impanns, false);
}
// 六、访问方法的属性信息
while (attributes != null) {
Attribute attr = attributes.next;
attributes.next = null;
mv.visitAttribute(attributes);
attributes = attr;
}
// 七、访问方法代码对应的字节码信息
if (code != 0) {
mv.visitCode();
readCode(mv, context, code);
}
// 八、visits the end of the method
mv.visitEnd();
return u;
}
复制代码
同类和字段的读取、访问套路同样,首先,在注释1和注释2处,会 先开始进行方法相关的字节码解析的工做:读取了方法的描述和属性信息。而后,在注释3 ~ 注释7处 按顺序访问了方法的描述、参数、注解、属性、方法代码对应的字节码信息。须要注意的是,在 readCode 方法中,也是先读取了方法内部代码的字节码信息,例如头部、属性等等,而后,便会访问对应的指令集。最后,在注释8处 调用了 MethodVisitor 实例的 visitEnd 方法结束了方法信息的访问。
从以上对 ClassVisitor 与 ClassReader 的分析看来,ClassVisitor 被定义为了一个能接收并解析 ClassReader 传入信息的类。当在 accpet 方法中 ClassVisitor 访问 ClassReader 时,ClassReader 便会先开始字节码的解析工做,并将保存在内存中的结果源源不断地经过调用各类 visitxxx 方法传入到 ClassVisitor 之中。
须要注意的是,其中 只有 visit 这个方法必定会被调用一次,由于它 获取了类头部的描述信息,显然易见,它必不可少,而对于其它的 visitxxx 方法来讲都不能肯定。例如其中的 visitMethod 方法,只有当 ClassReader 解析出一个方法的字节码时,才会调用一次 visitMethod 方法,并由今生成一个方法访问者 MethodVisitor 的实例。
而后,这个 MethodVisitor 的实例便会同 ClassVisitor 同样开始访问当前方法的属性信息,对于 ClassVisitor 来讲,它只处理和类相关的事,而方法的事情被外包给了 MethodVisitor 进行处理。这正是访问者的一大优点:将访问一个复琐事物的职责经过各个不一样类型但又相互关联的访问者分割开来。
由前可知,对象模型是事件模型的一个封装。其中的 ClassNode 其实就是 ClassVisitor 的一个子类,它负责将 ClassReader 传进来的信息进行分类储存。一样,MethodNode 也是 MethodVisitor 的一个子类,它负责将 ClassReader 传进来的操做码指令信息链接成一个列表并保存其中。
而 ClassWriter 也是 ClassVisitor 的一个子类,可是,它并不会储存信息,而是立刻会将传入的信息转译成字节码,并在以后随时输出它们。对于 ClassReader 这个被访问者来讲,它负责读取咱们传入的类文件中的字节流数据,并提供解析流中包含的一切类属性信息的操做。
最后,为了更进一步地将咱们上面所讲解的 ClassReader 与 ClassVisitor 的工做机制更加形象化,这里借用 hakugyokurou 的一张流程图用于回顾梳理,以下所示:
注意:第二个"实例化,经过构造函数..."须要去掉
ASM Core API 相似于解析 XML 文件中的 SAX 方式,直接用流式的方法来处理字节码文件,而不须要把这个类的整个结构读进内存之中。其好处是可以尽量地节约内存,难度在于编程时须要有必定的 JVM 字节码基础。因为它的性能较好,因此一般状况下咱们都会直接使用 Core API。下面,咱们再来回顾下 事件模型中 Core API 的关键组件,以下所示:
ClassReader
:用于读取已经编译好的 .class 文件。ClassWriter
:用于从新构建编译后的类,如修改类名、属性以及方法,也能够生成新的类的字节码文件。各类 Visitor 类
:如上所述,Core API 根据字节码从上到下依次处理,对于字节码文件中不一样的区域有不一样的 Visitor,好比用于访问方法的 MethodVisitor、用于访问类变量的 FieldVisitor、用于访问注解的 AnnotationVisitor 等等。为了实现 AOP,其重点是要灵活运用 MethodVisitor。在开始使用 ASM Core API 以前,咱们须要先了解一下 ASM Bytecode Outline
工具的使用。
当咱们使用 ASM 手写字节码的时候,一般会写一系列 visitXXXXInsn()
方法来写对应的助记符,因此 须要先将每一行源代码转化对应的助记符,而后再经过 ASM 的语法转换为与之对应的 visitXXXXInsn()。为了解决这个繁琐耗时的流程,所以,ASM Bytecode Outline 便应运而生。
首先,咱们须要安装 ASM Bytecode Outline gradle 插件,安装完成后,咱们就可 以直接在目标类中右键选择下拉框底部区域的 Show Bytecode outline,而后,AS 的右侧就会出现目标类对应的字节码与 ASM 信息查看区域。咱们直接 在新标签页中选择 ASMified 这个 tab 便可看到其与之对应的 ASM 代码,以下图所示:
为了更好地在实践中理解上面所学到的知识,咱们能够 使用 ASM 插桩实现方法耗时的统计 和 替换项目中全部的 new Thread。这里直接给出 Android 开发高手课的 ASM实战项目地址。
使用 ASM 编译插桩统计方法耗时主要能够细分为以下三个步骤:
刚开始的时候,咱们能够在 Application 的 onCreate 方法 先写下要插桩以后的代码,以下所示:
@Override
public void onCreate() {
long startTime = System.currentTimeMillis();
super.onCreate();
long endTime = System.currentTimeMillis() - startTime;
StringBuilder sb = new StringBuilder();
sb.append("com/sample/asm/SampleApplication.onCreate time: ");
sb.append(endTime);
Log.d("MethodCostTime", sb.toString());
}
复制代码
这样便于 以后能使用 ASM Bytecode Outline 的 ASMified 下的 Show differences 去展现相邻两次修改的代码差别,其修改以后 ASM 代码对比图以下所示:
在右图中所示的差别代码就是咱们须要添加的 ASM 代码。这里咱们直接使用 ASM 的事件模式,即 ASM 的 Core API 来进行字节码的读取与修改,代码以下所示:
ClassReader classReader = new ClassReader(is);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// 1
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
复制代码
上面的实现代码咱们在上面已经详细分析过了,当 classReader 调用 accept 方法时就会对类文件进行读取和被 classVisitor 访问。那么,咱们是如何对方法中的字节码进行操做的呢?
在注释1处,咱们 自定义了一个 ClassVisitor,其中的奥秘之处就在其中,其实现代码以下所示:
public static class TraceClassAdapter extends ClassVisitor {
private String className;
TraceClassAdapter(int i, ClassVisitor classVisitor) {
super(i, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
}
@Override
public void visitInnerClass(final String s, final String s1, final String s2, final int i) {
super.visitInnerClass(s, s1, s2, i);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className);
}
@Override
public void visitEnd() {
super.visitEnd();
}
}
复制代码
因为咱们只须要对方法的字节码进行操做,直接处理 visitMethod
这个方法便可。在这里咱们直接将类观察者 ClassVisitor 经过访问获得的 MethodVisitor 进行了封装,使用了自定义的 AdviceAdapter 的方式来实现,而 AdviceAdapter 也是 MethodVisitor 的子类,不一样于 MethodVisitor的是,它自身提供了 onMethodEnter 与 onMethodExit 方法,很是便于咱们去实现方法的先后插桩。其实现代码以下所示:
private int timeLocalIndex = 0;
@Override
protected void onMethodEnter() {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
// 1
timeLocalIndex = newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, timeLocalIndex);
}
@Override
protected void onMethodExit(int opcode) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LLOAD, timeLocalIndex);
// 此处的值在栈顶
mv.visitInsn(LSUB);
// 由于后面要用到这个值因此先将其保存到本地变量表中
mv.visitVarInsn(LSTORE, timeLocalIndex);
int stringBuilderIndex = newLocal(Type.getType("java/lang/StringBuilder"));
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
// 须要将栈顶的 stringbuilder 指针保存起来不然后面找不到了
mv.visitVarInsn(Opcodes.ASTORE, stringBuilderIndex);
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitLdcInsn(className + "." + methodName + " time:");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitInsn(Opcodes.POP);
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitVarInsn(Opcodes.LLOAD, timeLocalIndex);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitInsn(Opcodes.POP);
mv.visitLdcInsn("Geek");
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
// 注意: Log.d 方法是有返回值的,须要 pop 出去
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
// 2
mv.visitInsn(Opcodes.POP);
}
复制代码
首先,在 onMethodEnter 方法中的注释1处,咱们调用了 AdviceAdapter 的其中一个父类 LocalVariablesSorter 的 newLocal 方法,它会根据指定的类型建立一个新的本地变量,并直接分配一个本地变量的引用 index,其优点在于能够尽可能复用之前的局部变量,而不须要咱们考虑本地变量的分配和覆盖问题。而后,在 onMethodExit 方法中咱们即可以将以前的差别代码拿过来适当修改调试便可,须要注意的是,在注释2处,即 onMethodExit 方法的最后须要保证栈的清洁,避免在栈顶遗留下不使用的数据,若是在栈顶还留有数据的话,不只会致使后续代码的异常,也会对其余框架处理字节码形成影响,所以若是操做数栈还有数据的话须要消耗掉或者 POP 出去。
首先,咱们先将 MainActivity 的 startThread 方法里面的 Thread 对象改变成 CustomThread,而后经过 ASM Bytecode Outline 的 Show differences 查看在字节码上面的差别,以下图所示:
咱们注意到,这里首先调用了 NEW 操做码建立了 thread 实例,而后才调用了 InvokeVirtual 操做码去执行 thread 实例的构造方法。一般状况下这两条指令是成对出现的,可是,偶尔会遇到从其余某个位置传递过来一个已经存在的实例,并直接强制调用构造方法的状况。所以,咱们 须要在代码里面判断 new 和 InvokeSpecial 是不是成对出现的。其实现代码以下所示:
private final String methodName;
private final String className;
// 标识是否遇到了 new 指令
private boolean find = false;
protected TraceMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className) {
super(api, mv, access, name, desc);
this.className = className;
this.methodName = name;
}
@Override
public void visitTypeInsn(int opcode, String s) {
if (opcode == Opcodes.NEW && "java/lang/Thread".equals(s)) {
// 遇到 new 指令
find = true;
mv.visitTypeInsn(Opcodes.NEW, "com/sample/asm/CustomThread");
return;
}
super.visitTypeInsn(opcode, s);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
//须要排查 CustomThread 本身
if ("java/lang/Thread".equals(owner) && !className.equals("com/sample/asm/CustomThread") && opcode == Opcodes.INVOKESPECIAL && find) {
find = false;
mv.visitMethodInsn(opcode, "com/sample/asm/CustomThread", name, desc, itf);
Log.e("asmcode", "className:%s, method:%s, name:%s", className, methodName, name);
return;
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
复制代码
在使用 ASM 进行插桩的时候,咱们尤为须要注意如下 两点:
try catch blcok
的处理,关于异常处理可使用 ASM 提供的 CheckClassAdapter,能够在修改完成后验证一下字节码是否正常。除了直接使用 ASM 进行插桩以外,若是需求比较简单,咱们可使用基于 ASM 的字节码处理工具,例如:lancet、Hunter 和 Hibeaver,此时使用它们的投入产出比会更高。
在 ASM Bytecode Outline 工具的帮助下,咱们可以完成不少场景下的 ASM 插桩的需求,可是,当咱们使用其处理字节码的时候仍是须要考虑不少种可能出现的状况。若是想要具有这方面的深度思考能力,咱们就 必须对每个操做码的特征都有较深的了解,若是还不了解的同窗能够去看看 《深刻探索编译插桩技术(3、JVM字节码)。所以,要具有实现一个复杂 ASM 插桩的能力,咱们须要对 JVM 字节码、ASM 字节码以及 ASM 源码中的核心工具类的实现 作到了然于心,而且在不断地实践与试错以后,咱们才可以成为一个真正的 ASM 插桩高手。
一、ASM官方文档
二、极客时间之Android开发高手课 编译插桩的三种方法:AspectJ、ASM、ReDex
三、极客时间之Android开发高手课 练习Sample跑起来 | ASM插桩强化练习
四、AndroidAdvanceWithGeektime / Chapter07
九、AndroidAdvanceWithGeektime / Chapter-ASM
十、IntelliJ 插件 - ASM Bytecode Outline
十一、ASM封装库:lancet、Hunter 和 Hibeaver
十二、基于 Javassist 的字节码处理工具:DroidAssist
1三、除了编译期间修改 class 的方式,其实在运行期间咱们也能够生成代码,例如如今比较流行的运行时代码生成库 byte-buddy、byte-buddy 中文文档
欢迎关注个人微信:
bcce5360
因为微信群已超过 200 人,麻烦你们想进微信群的朋友们,加我微信拉你进群。
2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~