Android无痕埋点技术(一):ASM基础

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

编译过程

在Android开发中,编译过程概况为从源代码到字节码,再将字节码打包成dex,最后合并dex成为apk的过程。

第一种是APT(Annotation Processing Tool),即注解处理器,是一种处理注解的工具,也可以说是javac工具,用来在编译时扫描和处理注解,最终生成.java文件,可以减少很多重复的代码。常用的框架有ButterKnife、Dagger、EventBus等。

第二种是AspectJ,在java源文件生成字节码时进行操作。是对AOP(面向切面编程)的一个实践,AOP在Android甚至在java领域应用都十分的广泛,典型的应用场景比如:日志记录、性能监控、数据校验,缓存等。

第三种是ASM,对java字节码文件进行操作。是一款轻量级字节码操作库,记得网易乐得团队做过一个效率对比。ASM效率领先Javassist。

aop

字节码

Java 字节码(英语:Java bytecode)是Java虚拟机执行的一种指令格式。通俗来讲字节码就是经过javac命令编译之后生成的Class文件。Class文件包含了Java虚拟机指令集和符号表以及若干其他的辅助信息。Class文件是一组以8位字节为基础单位的二进制流,哥哥数据项目严格按照顺序紧凑的排列在Class文件之中,中间没有任何分隔符,这使得整个Class文件中存储的内容几乎全是程序运行时的必要数据。

字节码结构

占用的字节大小

class_info

  • Magic魔数:该项存放了一个 Java 类文件的魔数(magic number)和版本信息。一个 Java 类文件的前 4 个字节被称为它的魔数。每个正确的 Java 类文件都是以 0xCAFEBABE 开头的,这样保证了 Java 虚拟机能很轻松的分辨出 Java 文件和非 Java 文件。
  • Version:包括主版本号和次版本号,该项存放了 Java 类文件的版本信息,类文件的版本信息让虚拟机知道如何去读取并处理该类文件。
  • Constant Pool:该项存放了类中各种文字字符串、类名、方法名和接口名称、final 变量以及对外部类的引用信息等常量。虚拟机必须为每一个被装载的类维护一个常量池,常量池中存储了相应类型所用到的所有类型、字段和方法的符号引用。常量池的大小平均占到了整个类大小的 60% 左右。
  • Access_flag:该项指明了该文件中定义的是类还是接口(一个 class 文件中只能有一个类或接口),同时还指名了类或接口的访问标志,如 public,private, abstract 等信息。
  • This Class:指向表示该类全限定名称的字符串常量的指针。
  • Super Class:指向表示父类全限定名称的字符串常量的指针。
  • Interfaces:一个指针数组,存放了该类或父类实现的所有接口名称的字符串常量的指针。
  • Fields:该项对类或接口中声明的字段进行了细致的描述。需要注意的是,fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。
  • Methods:该项对类或接口中声明的方法进行了细致的描述。例如方法的名称、参数和返回值类型等。需要注意的是,methods 列表里仅存放了本类或本接口中的方法,并不包括从超类和父接口继承而来的方法。
  • Class attributes:该项存放了在该文件中类或接口所定义的属性的基本信息。

字节码类型对照表

字节码的读取与分析引擎。每当有事件发生时,调用注册的ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor做相应的处理。

Type Signature Java Type
Z boolean
B byte
C char
S short
I int
J long
F float
D double
L fully-qualified-class ;fully-qualified-class
[ type type[]
( arg-types ) ret-type method type
  • 比如:long f (int n, String s, int[] arr); 对应类型签名为:f (ILjava/lang/String;[I)J
  • 比如 : void hi(double a, List b);对应类型签名为:hi (DLjava/util/List;)V

ASM Core API

核心api调用流程如下

  • ASM 提供了一个类ClassReader可以方便地让我们对class文件进行读取与解析
  • ASM 在ClassReader解析class文件过程中,解析到某一个结构就会通知到ClassVisitor的相应方法(eg:解析到类方法时,就会回调ClassVisitor.visitMethod方法)
  • 可以通过更改ClassVisitor中相应结构方法返回值,实现对类的代码切入(eg:更改ClassVisitor.visitMethod()方法的默认返回值MethodVisitor实例,通过操作该自定义MethodVisitor从而实现对原方法的改写)
  • 其它的结构遍历也如同ClassVisitor
  • 通过ClassWriter的toByteArray()方法,得到class文件的字节码内容,最后通过文件流写入方式覆盖掉原先的内容,实现class文件的改写

ClassReader

这个类会提供你要转变的类的字节数组,它的accept方法,接受一个具体的ClassVisitor,并调用实现中具体的 visit,
visitSource, visitOuterClass, visitAnnotation, visitAttribute, visitInnerClass,visitField, visitMethod和 visitEnd 方法。ClassReader.accept(ClassVisitor classVisitor, int parsingOptions)中,第二个参数parsingOptions的取值有以下选项:

  • ClassReader.SKIP_DEBUG:表示不遍历调试内容,即跳过源文件,源码调试扩展,局部变量表,局部变量类型表和行号表属性,即以下方法既不会被解析也不会被访问(ClassVisitor.visitSource,MethodVisitor.visitLocalVariable,MethodVisitor.visitLineNumber)。使用此标识后,类文件调试信息会被去除,请警记。
  • ClassReader.SKIP_CODE:设置该标识,则代码属性将不会被转换和访问,例如方法体代码不会进行解析和访问。
  • ClassReader.SKIP_FRAMES:设置该标识,表示跳过栈图(StackMap)和栈图表(StackMapTable)属性,即MethodVisitor.visitFrame方法不会被转换和访问。当设置了ClassWriter.COMPUTE_FRAMES时,设置该标识会很有用,因为他避免了访问帧内容(这些内容会被忽略和重新计算,无需访问)。
  • ClassReader.EXPAND_FRAMES:该标识用于设置扩展栈帧图。默认栈图以它们原始格式(V1_6以下使用扩展格式,其他使用压缩格式)被访问。如果设置该标识,栈图则始终以扩展格式进行访问(此标识在ClassReader和ClassWriter中增加了解压/压缩步骤,会大幅度降低性能)。

ClassVisitor

一个可以访问Java类的访问者。其方法被调用次序必须满足:

1
2
3
4
5
visit visitSource? 
visitOuterClass?
( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd

AnnotationVisitor

定义在解析注解时会触发的事件。AnnotationVisitor api 访问时序如下:

1
2
( visit | visitEnum | visitAnnotation | visitArray )* 
visitEnd

FieldVisitor

定义在解析字段时触发的事件,如解析到字段上的注解、解析到字段相关的属性等。

MethodVisitor

ASM 生成和转换class文件方法使用的是抽象类MethodVisitor,ClassVisitor.visitMethod方法返回的就是该实例。

1
2
3
4
5
6
7
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd

即如果有annotations或者attributes,它们必须被第一个访问,接下来对于非抽象方法访问的就是方法内部字节码(visitCode),然后在visitCode和visitMaxs中的那些指令会按上面所示方法顺序访问,最后类方法访问结束回调visitEnd。

ClassWriter

这个类是ClassVisitor的一个实现类,这个类中的toByteArray方法会将最终修改的字节码以 byte 数组形式返回。它可以单独使用,也可以传递给一个或多个ClassReader或ClassVisitor适配器修改一个或多个已存在的Java类的类文件。

我们知道,类文件有着自己严格的格式,当我们想要注入相关代码时,不是直接注入相关指令就可以的,比如对于方法注入,我们可能还需要对栈帧图( stack map frames)进行计算:你需要计算所有的帧,找到有对象跳转或者绝对跳转的帧,最后还要压缩剩余的帧。同样,对于栈帧的局部变量表和操作数栈的大小也要自己进行计算。这些计算操作具备一定的难度,幸运的是,当我们创建一个ClassWriter时,可以配置 ASM 自动帮我们对指定的内容进行计算。ClassWriter的构造函数需要传入一个 flag,其含义为:

  • ClassWriter(0):表示 ASM 不会自动自动帮你计算栈帧和局部变量表和操作数栈大小。

  • ClassWriter(ClassWriter.COMPUTE_MAXS):表示 ASM 会自动帮你计算局部变量表和操作数栈的大小,但是你还是需要调用visitMaxs方法,但是可以使用任意参数,因为它们会被忽略。带有这个标识,对于栈帧大小,还是需要你手动计算。

  • ClassWriter(ClassWriter.COMPUTE_FRAMES):表示 ASM 会自动帮你计算所有的内容。你不必去调用visitFrame,但是你还是需要调用visitMaxs方法(参数可任意设置,同样会被忽略)。

    使用这些标识很方便,但是会带来一些性能上的损失:COMPUTE_MAXS标识会使ClassWriter慢10%,COMPUTE_FRAMES标识会使ClassWriter慢2倍,

ASM Bytecode outline/viewer

在Android Studio中,选择Preference=>Plugins,搜索并安装ASM Bytecode Outline插件,重启AndroidStudio后生效。

在编译器中,选择想要查看的java文件右键,选择show bytecode outline/ASM bytecode viewer即可查看,

asm_plugin

参考文章

https://www.jianshu.com/p/e5062d62a3d1

https://www.jianshu.com/p/abd1b1b8d3f3

https://www.ibm.com/developerworks/cn/java/j-lo-asm30/index.html

https://juejin.im/post/5aa0e7eff265da2395308f48

您的支持是我原创的动力