2024年最全深入探索编译插桩技术(四、ASM 探秘)(1),初级android面试题

最后

这里我希望可以帮助到大家提升进阶。

内容包含:Android学习PDF+架构视频+面试文档+源码笔记高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这几块的内容。非常适合近期有面试和想在技术道路上继续精进的朋友。

喜欢本文的话,不妨给我点个小赞、评论区留言或者转发支持一下呗~

img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

这里我们 直接调用了 InsnList 的 set 方法就能替换指定的操作码对象,我们在获取了 “BIPUSH 64” 字节码的位置后,便将封装它的操作码替换为一个新的 VarInsnNode 操作码,这个新操作码封装了 “ALOAD 1” 字节码, 将原程序中 将值设为16 替换为 将值设为局部变量1

3、删除指定的操作码

methodNode.instructions.remove(xxx);

xxx 表示的是要删除的操作码实例,我们直接调用用 InsnList 的 remove 方法将它移除掉即可。

4、插入指定的操作码

InsnList 主要提供了 四类 方法用于插入字节码,如下所示:

  • 1)、add(AbstractInsnNode insn)将一个操作码添加到 InsnList 的末尾
  • 2)、insert(AbstractInsnNode insn)将一个操作码插入到这个 InsnList 的开头
  • 3)、insert(AbstractInsnNode insnNode,AbstractInsnNode insn)将一个操作码插入到另一个操作码的下面
  • 4)、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 类来输出字节码,代码如下所示:

// 1、让 ClassWriter 自行计算最大栈深度和栈映射帧等信息
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTEFRAMES);
classNode.accept(classWriter);
return classWriter.toByteArray();

关于 ClassWriter 的具体用法,我们会在 ASM Core API 这部分来进行逐步讲解。下面👇,我们就先来看看 ASM 的事件模型。

三、ASM 的事件模型(ASM Core API)

对象模型是由事件模型封装而成,因此事件模型的上手难度会更大一些。

对于事件模型来说,它 采用了设计模式中的访问者模式。它的出现是为了更好地解决这样一种需求:有 A 个元素和 N 种算法,每个算法都能作用于任意一个元素,并且在不同的元素上有不同的运行方式

在访问者模式出现之前,我们通常会在每一个元素对应的类中添加 N 个方法,然后在每一个方法中去实现一个算法,但是,这样的做法容易导致代码耦合性过高,并且可维护性差。

因此,访问者模式应运而生,我们可以 建立 N 个访问者,并且每一个访问者拥有一个算法及其内部的 A 种运行方式。当我们需要调用一个算法时,就让相应的访问者去访问元素,然后让访问者根据被访问对象选择相应的算法

需要注意的是,访问者并没有直接去操作元素,而是先让元素类调用 accept 方法接收访问者,然后,访问者在元素类的内部方法中开始调用 visit 方法访问当前的元素类。这样,访问者便能直接访问元素类中的内部私有成员,其优势在于 避免了暴露不必要的内部细节

要理解 ASM 的事件模型,我们就需要对其中的 两个重要成员的工作原理 有较深的了解。它们便是 类访问者 ClassVisitor 与 类读取(解析)者 ClassReader

从字节码的视角中,一个 Java 类由很多组件凝聚而成,而这之中便包括超类、接口、属性、域和方法等等。当我们在使用 ASM 进行操控时,可以将它们视为一个个与之对应的事件。因此 ASM 提供了一个 类访问者 ClassVisitor,以通过它来访问当前类的各个组件,当解析器 ClassReader 依次遇到上述的各个组件时,ClassVisitor 上对应的 visitor 事件处理器方法均会被一一调用

与类相似,方法也是由多个组件凝聚而成的,其对应着方法属性、注解及编译后的代码(Class 字节码)。ASM 的 MethodVisitor 提供了一种 hook(钩子)机制,以便能够访问方法中的每一个操作码,这样我们便能够对字节码文件进行细粒度地修改

下面,我们便来一一分析下它们。

1、类访问者 ClassVisitor

通常我们在使用 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),它到底是什么呢?

栈映射帧 StackMap Frame

它是 Java 6 以后引入的一种验证机制,用于 检验 Java 字节码的正确性。它的工作方式是 记录每一个关键步骤完成后其方法中操作数栈的理论状态,然后,在实际运行的时候,ASM 会将其实际状态和理论状态对比,如果状态不一致则表明出现了错误

但栈映射帧的实现并不简单,因此通过调用 classReader 实例的 accept 方法我们便可以让 ASM 自动去计算栈映射帧,尽管这 会增加 50% 的额外运算。此外,可能会有小概率的情况遇到 栈映射帧验证失败 的情况,例如:VerifyError: Inconsistent stackmap frames at branch target 这个错误。

最常见的原因可能就是由于 字节码写错造成的,此时,我们应该去检查对应的字节码实现代码。此外,也可能是 JDK 版本的支持问题或是 ASM 自身的缺陷,但是,这种情况几乎不会发生。

2、类读取(解析)者 ClassVisitor

现在,让我们再回到上述注释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;

// 1、读取类的描述信息,例如 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;
}

// 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);
}

// 3、访问类的描述信息
classVisitor.visit(readInt(items[1] - 7), access, name, signature,
superClass, interfaces);

// 4、访问源码和 debug 信息
if ((flags & SKIP_DEBUG) == 0
&& (sourceFile != null || sourceDebug != null)) {
classVisitor.visitSource(sourceFile, sourceDebug);
}

// 5、访问外部类
if (enclosingOwner != null) {
classVisitor.visitOuterClass(enclosingOwner, enclosingName,
enclosingDesc);
}

// 6、访问类注解和类型注解
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));
}
}

// 7、访问类的属性
while (attributes != null) {
Attribute attr = attributes.next;
attributes.next = null;
classVisitor.visitAttribute(attributes);
attributes = attr;
}

// 8、访问内部类
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;
}
}

// 9、访问字段和方法
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 标识已访问结束

1)、类内字段的解析

这里,我们先来看看 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) {
    // 1、读取字段的描述信息
    char[] c = context.buffer;
    int access = readUnsignedShort(u);
    String name = readUTF8(u + 2, c);
    String desc = readUTF8(u + 4, c);
    u += 6;

// 2、读取字段的属性
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;

// 3、访问字段的声明
FieldVisitor fv = classVisitor.visitField(access, name, desc,
signature, value);
if (fv == null) {
return u;
}

// 4、访问字段的注解和类型注解
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));
}
}

// 5、访问字段的属性
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 方法结束了字段信息的访问

2)、类内方法的解析

下面,我们在看看 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) {
    // 1、读取方法描述信息
    char[] c = context.buffer;
    context.access = readUnsignedShort(u);
    context.name = readUTF8(u + 2, c);
    context.desc = readUTF8(u + 4, c);
    u += 6;

// 2、读取方法属性信息
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;

// 3、访问方法描述信息
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 writers 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;
    }
    }
    }

// 4、访问方法参数信息
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));
}
}

// 5、访问方法的注解信息
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);
}

// 6、访问方法的属性信息
while (attributes != null) {
Attribute attr = attributes.next;
attributes.next = null;
mv.visitAttribute(attributes);
attributes = attr;
}

// 7、访问方法代码对应的字节码信息
if (code != 0) {
mv.visitCode();
readCode(mv, context, code);
}

// 8、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 的一张流程图用于回顾梳理,如下所示:

注意:第二个"实例化,通过构造函数…"需要去掉

3、小结

ASM Core API 类似于解析 XML 文件中的 SAX 方式,直接用流式的方法来处理字节码文件,而不需要把这个类的整个结构读进内存之中。其好处是能够尽可能地节约内存,难度在于编程时需要有一定的 JVM 字节码基础。由于它的性能较好,所以通常情况下我们都会直接使用 Core API。下面,我们再来回顾下 事件模型中 Core API 的关键组件,如下所示:

  • 1)、ClassReader用于读取已经编译好的 .class 文件
  • 2)、ClassWriter用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件
  • 3)、各种 Visitor 类如上所述,Core API 根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的 Visitor,比如用于访问方法的 MethodVisitor、用于访问类变量的 FieldVisitor、用于访问注解的 AnnotationVisitor 等等。为了实现 AOP,其重点是要灵活运用 MethodVisitor

四、综合实战训练

在开始使用 ASM Core API 之前,我们需要先了解一下 ASM Bytecode Outline 工具的使用。

1、使用 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 Outline 的类引用了工程中其它的 .java 文件,需要先使用 ASM Outline 将其编译成 .class 文件。

2、使用 ASM 编译插桩统计方法耗时

使用 ASM 编译插桩统计方法耗时主要可以细分为如下三个步骤:

  • 1)、首先,我们需要通过自定义 gradle plugin 的形式来干预编译过程
  • 2)、然后,在编译过程中获取到所有的 class 文件和 jar 包,然后遍历他们
  • 3)、最后,利用 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 代码对比图如下所示:

最后

针对于上面的问题,我总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。
Android进阶视频+面试资料部分截图

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

SampleApplication.onCreate time: ");
sb.append(endTime);
Log.d(“MethodCostTime”, sb.toString());
}

这样便于 之后能使用 ASM Bytecode Outline 的 ASMified 下的 Show differences 去展示相邻两次修改的代码差异,其修改之后 ASM 代码对比图如下所示:

[外链图片转存中…(img-9EDc0UHC-1715899803973)]

最后

针对于上面的问题,我总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。
[外链图片转存中…(img-1QeOAeXm-1715899803974)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 18
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值