使用ASM完成编译时插桩
ASM,是一个跟AspectJ功能类似比AspectJ更强大的编译时插桩框架。功能虽强大,不过用起来比AspectJ麻烦不少。
其实这个框架在Java中用的很多,对于Android开发者来说如果之前没有开发过Java就有点陌生了
官网 https://asm.ow2.io/
- ASM是一个通用的Java字节码操作和分析框架,可以用它来动态的生成类后者增强现有类的功能。
- ASM可以直接产生二进制的class文件,也可以在类被加载到Java虚拟机之前动态改变类的行为。
- Java Class的类文件的信息存储在.class文件中,ASM可以读取.class文件中的类信息,改变类行为,分析类信息,甚至生成新的类。
Andorid java文件打包流程:
.java文件->.class文件->.dex文件。想要编译时插桩一般有两种方式
- 更改java文件:APT,AndroidAnnotation 都是这个层面的dagger,butterknife等框架就是这个层面的应用。
- 更改class文件:AspectJ,ASM,javassisit等,功能更加强大
下面练习一个小例子,使用ASM来统计Application中onCreate执行的时间。
我们需要两大步来完成:
第一步拿到所有的.class文件,第二步交给ASM动态插入代码。
第一步找到class文件
如何能拿到呢?Google官方在Adnroid Gradle1.5.0版本提供了Transform API,允许第三方Plugin在打包dex文件之前的编译过程中操作.class文件。所以我们就可以使用Transform,拿到所有的.class文件。
想要使用Transform API,这时候就得自定义一个Gradle的插件了
- 新建一个项目,然后建一个新的module来写插件代码
- 因为gradle是用groovy写的,所以需要在main文件夹下在新建一个入口文件夹groovy。
- 告诉gradle哪个是我们自定义的插件,在main目录下新建resources目录,然后在resources目录里面再新建META-INF目录,再在META-INF里面新建gradle-plugins目录。最后在gradle-plugins目录里面新建properties文件
- properties文件的名字可以随便取,后面用到的时候就用这个取好的名字。在properties文件中指明我们自定义的插件的类
implementation-class=com.hsm.asmplugin.AsmPlugin
最后的目录结构是这样的

下面去到当前module下面的build.gradle添加相关的依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| apply plugin: 'groovy' apply plugin: 'maven'
repositories { mavenCentral() jcenter() } dependencies { implementation gradleApi() implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.5.0' implementation 'org.ow2.asm:asm:7.1' implementation 'org.ow2.asm:asm-commons:7.1' }
group='com.chs.asm.plugin' version='1.0' uploadArchives { repositories { mavenDeployer { repository(url: uri('./my-plugin'))
} } }
|
因为打包的时候需要用到maven,所以添加maven相关的依赖,uploadArchives是将已经自定义好了插件打包到本地Maven库里面去,也可以打包到远程服务器。group和version是使用的时候需要的组名和版本信息。
配置完成了,下面开始写代码,在groovy文件夹下写我们自己的插件继承Plugin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package com.hsm.asmplugin
import com.android.build.gradle.AppExtension import org.gradle.api.Plugin import org.gradle.api.Project import org.jetbrains.annotations.NotNull public class AsmPlugin implements Plugin<Project> {
@Override public void apply(@NotNull Project project) { def android = project.extensions.getByType(AppExtension) println '----------- 开始注册 >>>>> -----------' AsmTransform transform = new AsmTransform() android.registerTransform(transform) } }
|
获取project中的AppExtension类型extension,然后注册我们自己定义的Transform。
啥是AppExtension,我们app的gradle中最上面都有这个插件apply plugin: 'com.android.application
如果依赖了这个插件,AppExtension就存在。
下面来看AsmTransform
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
| package com.hsm.asmplugin
import com.android.build.api.transform.* import com.android.build.gradle.internal.pipeline.TransformManager import com.chs.asm.LogVisitor import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.io.FileUtils import org.apache.commons.io.IOUtils import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassWriter
import java.util.jar.JarEntry import java.util.jar.JarFile import java.util.jar.JarOutputStream import java.util.zip.ZipEntry
public class AsmTransform extends Transform {
@Override public String getName() { return "AsmTransform" } @Override public Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS; } @Override public Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT }
@Override public boolean isIncremental() { return false }
@Override public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { long startTime = System.currentTimeMillis() println '----------- startTime <' + startTime + '> -----------' Collection<TransformInput> inputs = transformInvocation.inputs; TransformOutputProvider outputProvider = transformInvocation.outputProvider; if (outputProvider != null) { outputProvider.deleteAll() } inputs.each { TransformInput input -> input.directoryInputs.each { DirectoryInput directoryInput -> handDirectoryInput(directoryInput, outputProvider) } input.jarInputs.each { JarInput jarInput -> handJarInput(jarInput, outputProvider) } } }
private static void handDirectoryInput(DirectoryInput input, TransformOutputProvider outputProvider) { if (input.file.isDirectory()) { input.file.eachFileRecurse { File file -> String name = file.name if ("MyApp.class".equals(name)) { ClassReader classReader = new ClassReader(file.bytes) ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) ClassVisitor classVisitor = new LogVisitor(classWriter) classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES) byte[] code = classWriter.toByteArray() FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + name) fos.write(code) fos.close() } } } def dest = outputProvider.getContentLocation(input.name, input.contentTypes, input.scopes, Format.DIRECTORY) FileUtils.copyDirectory(input.file, dest) } private static void handJarInput(JarInput jarInput, TransformOutputProvider outputProvider) { if (jarInput.file.getAbsolutePath().endsWith(".jar")) { def jarName = jarInput.name def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, jarName.length() - 4) } JarFile jarFile = new JarFile(jarInput.file) Enumeration enumeration = jarFile.entries() File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar") if (tmpFile.exists()) { tmpFile.delete() } JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile)) while (enumeration.hasMoreElements()) { JarEntry jarEntry = (JarEntry) enumeration.nextElement() String entryName = jarEntry.getName() ZipEntry zipEntry = new ZipEntry(entryName) InputStream inputStream = jarFile.getInputStream(jarEntry) if ("androidx/fragment/app/FragmentActivity.class".equals(entryName)) { println '----------- jar class <' + entryName + '> -----------' jarOutputStream.putNextEntry(zipEntry) ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream)) ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) ClassVisitor cv = new LogVisitor(classWriter) classReader.accept(cv, ClassReader.EXPAND_FRAMES) byte[] code = classWriter.toByteArray() jarOutputStream.write(code) } else { jarOutputStream.putNextEntry(zipEntry) jarOutputStream.write(IOUtils.toByteArray(inputStream)) } jarOutputStream.closeEntry() } jarOutputStream.close() jarFile.close() def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR) FileUtils.copyFile(tmpFile, dest) tmpFile.delete() } } }
|
上面类上的注释很清楚啦,Transform的inputs有两种类型,一种是源码目录,一种是jar包,分别遍历这两个,找到我们需要处理的class类型。比如上面的代码中源码部遍历筛选的是我们自己的Application->MyApp.class。jar包部分处理所有的FragmentActivity。这些都可以根据自己的需求来筛选。
然后通过ClassReader读取,通过ClassWriter交给我们自定义的类访问器LogVisitor来处理
ASM核心类
- ClassReader 用来解析编译过的字节码文件
- ClassWriter 用来重新构建编译后的类,比如修改类名,属性,方法或者生成新类的字节码文件
- ClassVisitor 用来访问类成员信息,包括标记在类上的注解,类的构造方法,类的字段,方法,静态代码块
- MethodVisitor 用来访问方法的信息,用来进行具体的方法字节码操作。
- AdviceAdapter 用来访问方法的信息,用来进行具体的方法字节码操作。是MethodVisitor的增强实现
第二步动态插入代码
第一步中通过Transform,遍历所有的class文件,筛选出我们想要处理的class,然后交给了类访问器来处理,下面就来看怎么处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package com.chs.asm;
import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes;
public class LogVisitor extends ClassVisitor { private String mClassName; public LogVisitor(ClassVisitor classVisitor) { super(Opcodes.ASM5, classVisitor); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { System.out.println("LogVisitor : visit -----> started:" + name); this.mClassName = name; super.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); if ("com/hsm/asmtext/MyApp".equals(this.mClassName)) { if ("onCreate".equals(name)) { System.out.println("LogVisitor : visitMethod method ----> " + name); return new OnCreateVisitor(mv); } } return mv; } @Override public void visitEnd() { System.out.println("LogVisitor : visit -----> end"); super.visitEnd(); } }
|
在visitMethod方法中筛选出我们想要操作的方法。比如这里操作onCreate方法。筛选出来之后交给自定义的方法访问者OnCreateVisitor来处理
怎么处理呢?假如我们想要在Application的onCreate方法执行前插入一行记录时间的代码,在onCreate之后在插入一行代码如下
1 2 3 4 5 6 7 8 9 10
| public class MyApp extends Application {
@Override public void onCreate() { long startTime = System.currentTimeMillis(); super.onCreate(); ...一堆操作.. long interval = System.currentTimeMillis()-startTime; } }
|
那么使用ASM插入的方式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| package com.chs.asm;
import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes;
public class OnCreateVisitor extends MethodVisitor {
public OnCreateVisitor(MethodVisitor methodVisitor) { super(Opcodes.ASM5, methodVisitor); } @Override public void visitCode() { super.visitCode();
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); mv.visitVarInsn(Opcodes.LSTORE, 1); }
@Override public void visitInsn(int opcode) { if (opcode == Opcodes.RETURN) { mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); mv.visitVarInsn(Opcodes.LLOAD, 1); mv.visitInsn(Opcodes.LSUB); mv.visitVarInsn(Opcodes.LSTORE, 3); Label l3 = new Label(); mv.visitLabel(l3); mv.visitLineNumber(20, l3); mv.visitLdcInsn("TAG"); mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); mv.visitInsn(Opcodes.DUP); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false); mv.visitLdcInsn("interval:"); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); mv.visitVarInsn(Opcodes.LLOAD, 3); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false); mv.visitInsn(Opcodes.POP); } super.visitInsn(opcode); }
@Override public void visitEnd() { super.visitEnd(); } }
|
其实到这里就完事了,之后就是打包发布到maven然后供我们的主工程使用了,不过上面的代码是个什么鬼,啥意思啊,我们该怎么写出来啊。
想要弄懂上面的代码,需要对java字节码和JVM的指令有一定的了解,上面就是组装一个方法的代码,需要用到包名啊,方法签名等。
如果我们不了解JVM指令可以写出上面的代码吗?当然可以,有牛逼的前辈早已经写出了插件来生成这样的代码,上面的代码就是生成出来的。
打开AndroidStudio的安装插件的界面,搜索ASM Bytecode Outline这个插件安装。
怎么使用呢
先写一个空的Application如下
1 2 3 4 5 6 7
| public class MyApp extends Application {
@Override public void onCreate() { super.onCreate(); } }
|
然后鼠标右击,选择Show Bytecode Outline,就能看到这几行代码的字节码了。

然后在里面加上我们要插入的代码,在执行同样的操作
1 2 3 4 5 6 7 8 9 10
| public class MyApp extends Application {
@Override public void onCreate() { long startTime = System.currentTimeMillis(); super.onCreate(); long interval = System.currentTimeMillis()-startTime; Log.i("TAG","interval:"+interval); } }
|
又能看到当前几行代码的字节码。然后牛逼的功能又来了,点击ASMified这个tab,里面有个show differences

就能看到前后两次操作生成的指令的区别在哪里了。这样就能很清晰的知道该怎么写了如下。

OK,下面开始发布上传,之前build.gradle中已经配置好了maven的本地仓库地址了。下面直接使用AndroidSrudio的快捷键上传
打开最右边的Gradle面板,然后点击uploadArchives上传,

OK之后就可以在对应目录看到我们上传的jar包了。本项目配置的仓库在当前目录下的my-plugin文件夹,最后生成目录如下

在当前工程目录引入本地maven仓库地址和我们自己写的插件,插件的包名和版本号就是之前插件build.gradle中配置的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| buildscript { repositories { google() jcenter() maven { url uri('D:/androiddemo/5/ASMText/asmplugin/my-plugin') } } dependencies { classpath 'com.android.tools.build:gradle:3.5.0' classpath 'com.chs.asm.plugin:asmplugin:1.0' } }
allprojects { repositories { google() jcenter() maven { url uri('D:/androiddemo/5/ASMText/asmplugin/my-plugin') } } }
|
然后去app中build.gradle中引入插件
1
| apply plugin: 'com.chs.asm-plugin'
|
OK大工完成,在onCreate中添加个耗时代码用来测试,运行app
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class MyApp extends Application {
@Override public void onCreate() { super.onCreate(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
|
运行结果如下
1
| 2019-09-26 11:09:28.838 28745-28745/com.hsm.asmtext I/TAG: interval:1002
|
到这里ASM的简单用法就算入门了,想要自如的操控我们的代码,还需要继续系统的学习一下gradle和ASM的知识。多多练习多熟悉。
参考博文
在AndroidStudio中自定义Gradle插件
【Android】函数插桩(Gradle + ASM)
Android ASM自动埋点方案实践