在之前android的性能监测sdk项目中用到过asm库,在这里记录一下基本原理和用法;


ASM库/工具  http://asm.ow2.org/

ASM是一款基于java字节码层面的代码分析和修改工具;无需提供源代码即可对应用嵌入所需debug代码,用于应用API性能分析,代码优化和代码混淆等工作。ASM的目标是生成,转换和分析已编译的java class文件,可使用ASM工具读/写/转换JVM指令集。

ASM工具提供两种方式来产生和转换已编译的class文件,它们分别是基于事件和基于对象的表示模型。其中,基于事件的表示模型使用一个有序的事件序列表示一个class文件,class文件中的每一个元素使用一个事件来表示,比如class的头部,变量,方法声明,JVM指令都有相对应的事件表示,ASM使用自带的事件解析器能将每一个class文件解析成一个事件序列。而基于对象的表示模型则使用对象树结构来解析每一个class文件。

基于事件模型的ASM工具使用生产者-消费者模型转换/产生一个class文件。其转换过程中涉及到自定义的事件生产者,自定义的事件过滤器和自定义的事件消费者这三种组件。其中使用classReader来解析每一个class文件中的事件元素,使用自定义的各种基于方法/变量/声明/类注释的元素适配器来过滤和修改class事件元序列中的相应事件对象,最后使用ClassWriter来重新将更新后的class事件序列转换成class字节码供JVM加载执行。整个生产/转换class文件的过程如下图所示,起点和终点分别是ClassReader(Class文件解析器)和ClassWriter(class事件序列转换到class字节码),中间的过程由若干个自定义的事件过滤器组成。

1.1、class文件的结构
class文件保持固定的结构信息,而且保留了几乎所有的源代码文件中的符号。一个class文件整体结构由几个区域组成,一个区域用来描述类的modifier,name,父类,接口和注释。一个区域用来描述类中变量的modfier,名字,类型和注释。一个区域用来描述类中方法和构造函数的modifier,名字参数类型,返回类型,注释等信息,当然也包含已编译成java字节码指令序列的方法具体内容。还有一个作为class文件的静态池区域,用来保存所有的数字,字符串,类型的常量,这些常量只被定义过一次且被其他class中区域所引用。class文件与源代码文件的关系:一个java文件最后会被编译成N(1 <= N)个class文件。
下图展示了一个class文件的总体概貌

图 1.1 class文件的概览(*表示0个或者多个)

1.2、 class文件的内部命名
原java类型与class文件内部类型对应关系

图1.2 java类型的描述

        原java方法声明与class文件内部方法声明的对应关系

图1.3方法描述举例

1.3、ASM工具的接口和组件
ASM工具生产和转换class文件内容的所有工作都是基于ClassVisitor这个抽象类进行的。ClassVisitor抽象类中的每一个方法会对应到class文件的相应区域,每个方法负责处理class文件相应区域的字节码内容。下图展示了ClassVisitor抽象类的成员函数。

某些区域方法直接完成该区域字节码内容的处理并返回空,某些复杂class区域的方法需返回更加细节的XXXVisitor对象,此XXXVisitor对象将负责处理此class区域的更加细节的字节码内容,开发者可以编写继承自XXXVisitor抽象类的自定义类,在成员函数中实现对细节字节码操作的逻辑代码。比如,visitField方法用来负责class文件中变量区域的字节码内容修改,该区域又可细分出多种属性数据对象(注释,参数值),这里需要编写继承自fieldVisitor抽象类的自定义类完成这些细分数据对象的字节码内容操作。下图为FieldVisitor的抽象类内容。

另外,在处理整个class文件处理过程中,classVistor抽象类中的方法访问需满足下面的次序。
visitvisitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*
(visitInnerClass | visitField | visitMethod )*
visitEnd
基于ClassVistor api的访问方式,ASM工具提供了三种核心组件用来实现class的产生和转换工作。ClassReader负责解析class文件字节码数组,然后将相应区域的内容对象传递给classVistor实例中相应的visitXXX方法,ClassReader可以看作是一个事件生产者。ClassWriter继承自ClassVistor抽象类,负责将对象化的class文件内容重构成一个二进制格式的class字节码文件,ClassWriter可以看作是一个事件消费者。继承自ClassVistor抽象类的自定义类负责class文件各个区域内容的修改和生成,它可以看作是一个事件过滤器,一次生产消费过程中这样的事件过滤器可以有N个(0<=N)。

1.3.1、遍历CLASS字节码类信息
面的例子用来打印class字节码内容的类信息,这里以java.lang.runnable为例。
test.java:

输出:

1.3.2、生产自定义类对应的class字节码内容
目标生产出以下自定义接口:

test.java内容:

1.3.3、动态加载1.3.2生产出的class字节码并实例化该类
使用继承自ClassLoader的类,并重写defineClass方法;
test.java:

使用继承自ClassLoader的类,并重写findClass内部类;
test.java:

1.3.4、转换class字节码内容中类的成员(变量,方法,注释等)
1.3.4.1、ClassReader生产者生产的class字节码bytes可以被ClassWriter直接消费,比如:

1.3.4.2、ClassReader生产者生产的class字节码bytes可以先被继承自ClassVisitor的自定义类过滤,最后被ClassWriter消费,比如:

具体架构图如下如所示,

图1.6类字节码转换链(bytes->reader->adapter->writer->bytes)

这里的ChangeVersionAdapter继承自ClassVisitor,它没做其他过滤,仅仅重写了visit方法,过滤出类中的方法并指定方法的版本号(v1.5)。

整个字节码转换过程的时序图如下所示。

图1.7类字节码使用ChangeVersionAdapter过滤转换过程的时序图

通过重写ClassVisitor的visit方法并修改cv.visit方法中的其他参数,你还能做其他有趣的事情。比如,你能给这个class增加一个接口,改变这个class的名字等。

1.3.4.3、如何使用转换过的class类字节码
前面的小节提到,转换后的class b2能够保存到磁盘中或者通过ClassLoader动态加载。通过这种动态加载的方式只能在本地使用这个ClassLoader加载的单个类,无法满足同时加载多个类的需求。如果你想要转换所有运行时的类字节码并能够加载使用这些转换后的类字节码文件(被systemClassLoader加载),你需要使用java.lang.instrument包中的ClassFileTransformer类。实现继承自ClassFileTransformer的自定义类并重写transform方法,在该方法中实现对所有系统class文件的遍历(这里的遍历指对各个class文件在字节码层面的转换)。另外这里的ClassFileTransformer必须在main方法之前完成class文件的转换,且必须与main方法在同一个JVM中运行,这样才能被system classLoader加载。
那么怎么实现字节码转换发生在main方法执行之前?这里涉及到java agent的概念,java代理(agent) 作为main方法前的一个拦截器 (interceptor),也就是在main方法执行之前,执行agent的代码。agent的代码与你的main方法在同一个JVM中运行,并被同一个system classloader装载,被同一的安全策略 (security policy) 和上下文 (context) 所管理。
怎样写一个java agent? 只需要实现premain这个方法
public static void premain(String agentArgs,Instrumentation inst)
下面的例子展示了如何实现对所有运行时class字节码进行转换并加载的方法。

1.3.5、删除class中的方法或者变量的做法
通过在visitMethod或者visitField方法中返回null的方式就能实现删除class中的方法或者变量的需求。如下实例,通过传入名字和描述删除class中的相应方法。

1.3.6、增加class中的方法或者变量的做法
在visitEnd中增加想要增加的方法或者变量,实例如下。

1.3.7、classVisitor过滤(转换)链
可在一次 生产—过滤–消费 过程中,使用多个继承自classVisitor的自定义过滤类对class进行过滤(转换)。

图2.8过滤链

下面的实例中通过传入多个自定义的过滤器,实现过滤链的功能。

1.3.8、最后通过一个例子来梳理一下asm工具的使用方法
例子中用到了agentmain,先补充一下相关知识。
agentmain方式
premain时JavaSE5开始就提供的代理方式,给了开发者诸多惊喜,不过也有些须不变,由于其必须在命令行指定代理jar,并且代理类必须在main方法前启动。因此,要求开发者在应用前就必须确认代理的处理逻辑和参数内容等等,在有些场合下,这是比较苦难的。比如正常的生产环境下,一般不会开启代理功能,但是在发生问题时,我们不希望停止应用就能够动态的去修改一些类的行为,以帮助排查问题,这在应用启动前是无法确定的。 为解决运行时启动代理类的问题,JavaSE6开始,提供了在应用程序的VM启动后在动态添加代理的方式,即agentmain方式。 与permain类似,agent方式同样需要提供一个agentjar,并且这个jar需要满足:
1. 在manifest中指定Agent-Class属性,值为代理类全路径
2. 代理类需要提供public static voidagentmain(String args, Instrumentation inst)或public static void agentmain(Stringargs)方法。并且再二者同时存在时以前者优先。args和inst和premain中的一致。
不过如此设计的再运行时进行代理有个问题——如何在应用程序启动之后再开启代理程序呢?JDK6中提供了JavaTools API,其中AttachAPI可以满足这个需求。
Attach API中的VirtualMachine代表一个运行中的VM。其提供了loadAgent()方法,可以在运行时动态加载一个代理jar。具体需要参考《Attach API》(http://docs.oracle.com/javase/6/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html)

完整的例子如下,在java.lang.ProcessBuilder实例调用start方法时eclipse的console中打印出”java.lang.ProcessBuilder实例的start方法被触发”。
代理类RewriterAgent.java:

自定义的ClassWriter:

测试类AttachTest: