介绍
ASM是一个通用的Java字节码操作和分析框架。它可以直接以二进制形式用于修改现有类或动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从中构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但是侧重于性能。因为它的设计和实现是尽可能的小和尽可能快,所以它非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。
上面这段话是摘自ASM官方的介绍,通俗的讲,ASM可以对现有的Java字节码进行增删改查,且更注重性能。
更多ASM详细的资料,可以参考其官网:
asm.ow2.io/
Gradle Transform介绍
上图是Android APP的构建流程,Gradle Transform是Android 官方从Gradle plugin 1.5.0-beta1版本开始包含的。它允许第三方插件在上图的".class Files" 到 "dex" 过程中用来分析和修改.class文件的一套标准API。
首先我们来了解下Transform的一些基础知识。
Transform类
我们先来了解下Transform
这个核心类,他的定义如下:
public abstract class Transform {
@NonNull
public abstract String getName();
@NonNull
public abstract Set<ContentType> getInputTypes();
@NonNull
public abstract Set<? super Scope> getScopes();
public abstract boolean isIncremental();
public void transform(@NonNull TransformInvocation transformInvocation)
throws TransformException, InterruptedException, IOException {
}
......
}
Transform
是一个抽象类,其中有四个方法是抽象方法,子类必须重写,分别为:
getName()
Transform的名称,且唯一,该名称会出现 app/build/intermediates/transforms 目录下。
getInputTypes()
定义该Transform需要处理的数据类型
CLASSES
:表示要处理编译后的java字节码,可能是jar包也有可能是文件夹RESOURCES
: 表示要处理标准的java资源
getScopes()
定义该Transform的作用范围
PROJECT
:只处理当前的moduleSUB_PROJECTS
:只处理子moduleEXTERNAL_LIBRARIES
:只处理当前module的依赖,如各种jar、aar包TESTED_CODE
:只处理测试代码,包含测试依赖PROVIDED_ONLY
:只处理以provided-only形式的依赖
isIncremental()
定义该Transform是否支持增量构建。
transform(TransformInvocation transformInvocation)
执行transform,在这里我们将对java字节码进行处理,其中我们要了解一下关键类:
TransformInput
:输入文件的类,它包含了:Collection<DirectoryInput>
:参与编译的文件夹下的源码文件Collection<JarInput>
:以jar包形式参与编译的文件,如远程或者本地的依赖
TransformOutputProvider
:文件输出类,可以获取输出的路径信息
Gradle Transform实践
下面我们来实现一个Gradle Transform,打印每个被transform的class文件。
第1步:新建一个Android project
project新建好后,再新建一个module,类型选择Java Library,命名autotracker-asm
第2步:将module类型更改为java-gradle-plugin
将autotracker-asm module下面的build.gradle更为java-gradle-plugin,并添加依赖。同时添加maven-publish,方便待会将plugin发布到mavenLocal。如下:
apply plugin: 'java-gradle-plugin'
apply plugin: 'maven-publish'
afterEvaluate {
publishing {
publications {
official(MavenPublication) {
groupId 'com.growingio.android'
artifactId 'autotracker-asm'
version '1.0.0'
from components.java
}
}
}
}
dependencies {
compileOnly gradleApi()
implementation 'com.android.tools.build:gradle:3.3.0'
}
第3步:创建Transform
类
class AutotrackTransform extends Transform {
@Override
public String getName() {
return "autotrackTransform";
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
// 只处理java class文件
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
// 处理整个project,包括依赖
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public boolean isIncremental() {
return false;
}
@Override
public void transform(TransformInvocation transformInvocation) throws IOException {
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
Collection<TransformInput> inputs = transformInvocation.getInputs();
// 遍历所有输入文件
for (TransformInput input : inputs) {
// 遍历所有文件夹
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
FluentIterable<File> allFiles = FileUtils.getAllFiles(directoryInput.getFile());
// 遍历文件夹下面的所有文件
for (File fileInput : allFiles) {
// 获取文件输出的目标文件夹
File outDir = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
outDir.mkdirs();
// 如果是java class文件,打印文件名
if (fileInput.getName().endsWith(".class")) {
System.err.println("Transformed class file " + fileInput.getName());
}
// 将文件拷贝到目标文件夹
FileUtils.copyFileToDirectory(fileInput,outDir);
}
}
// 遍历所有jar包
for (JarInput jarInput : input.getJarInputs()) {
// 获取jar包输出的目标文件
File jarOut = outputProvider.getContentLocation(jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
// 将jar包拷贝到目标文件
FileUtils.copyFile(jarInput.getFile(), jarOut);
}
}
}
}
第4步:创建Plugin类
public class AutotrackPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
AppExtension android = project.getExtensions().findByType(AppExtension.class);
// 注册Transform
android.registerTransform(new AutotrackTransform());
}
}
第5步:注册Plugin
在main文件夹下面创建目录 resources/META-INF/gradle-plugins,然后在此文件夹下面创建文件com.growingio.autotracker.properties,其中文件名com.growingio.autotracker就是我们的Plugin名称,即后续在build.gradle文件中添加的
apply plugin: 'com.growingio.autotracker'
其中properties文件内容为:
implementation-class=com.growingio.autotracker.asm.AutotrackPlugin
等号后面就是Plugin类的完整类名。
第6步:将Plugin发布到mavenLocal
执行task 'publishOfficialPublicationToMavenLocal',将Plugin发布到mavenLocal,最后的工程结构如下图所示。
第7步:添加Plugin
我们现在project根目录的build.gradle添加mavenLocal的repo和依赖,如下:
buildscript {
repositories {
mavenLocal()
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.0-alpha07"
classpath "com.growingio.android:autotracker-asm:1.0.0"
}
}
allprojects {
repositories {
mavenLocal()
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
在APP module下的build.gradle文件中应用插件
apply plugin: 'com.growingio.autotracker'
第8步:构建APP
我们点击APP 运行按钮,查看Build 窗口下的日志,如下:
> Task :app:transformClassesWithAutotrackTransformForDebug
Transformed class file MainActivity.class
Transformed class file BlankFragment.class
Transformed class file BuildConfig.class
我们发现Gradle Transform已经生效。
ASM基础知识
上文中我们已经找到了修改java字节码的切入点,那我们是不是可以开始实践AOP了呢?不急,由于ASM使用还是有一定门槛的,这里我们先简单的了解下ASM框架中几个核心类。
ClassReader
现有Java类的解析器。这个类主要解析符合Java类文件格式的字节码,在遇到每个属性、每个方法和字节码指令调用的时候调用相应的访问方法。
ClassWriter
以字节码的形式生成Java类的类访问器。更准确地说,这个类可以生成一个符合Java类文件格式的字节码。它可以单独使用,“从零开始”生成Java类,也可以与一个或多个ClassReader一起使用,从一个或多个现有Java类生成修改后的类。
ClassVisitor
访问Java类的访问器。主要负责解析Java类的注解、各种方法、各种属性等。
MethodVisitor
访问Java方法的访问器,主要负责解析和生成Java的方法。
GeneratorAdapter
MethodVisitor
的子类,封装了一些常用方法,用来方便的生成Java方法。
AdviceAdapter
GeneratorAdapter
的子类,和GeneratorAdapter
不同的是,他是一个抽象类,需要用户继承并重写。在方法访问之前、之后等时机执行相应的访问方法。
ASM实践
了解了基本知识以后,我们来实践一下,还是以上面的工程为基础,还是以之前的“在Fragment的onResume生命周期方法执行的时候织入代码”为案例。 上文中Gradle Transform的工程中,我们已经成功拦截到了每个类class文件,我们只需要将每一个class文件通过ASM框架进行解析、修改,最后再生成新的class文件不就行了吗?
第1步:定义织入代码
我们定义一个类和一个静态方法,待会在Fragment
的onResume
方法中执行该方法,该类在app的module中,如下:
public class FragmentAsmInjector {
private static final String TAG = "FragmentAsmInjector";
public static void afterFragmentResume(Fragment fragment) {
Log.e(TAG, "afterFragmentResume: fragment is " + fragment);
}
}
第2步:定义ClassVisitor
定义一个ClassVisitor,用来访问所有的java类,遇到Fragment子类的将织入代码。如下
package com.growingio.autotracker.asm;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
class FragmentAopClassVisitor extends ClassVisitor {
private boolean mIsFragmentClass = false;
private boolean mHasResumeMethod = false;
public FragmentAopClassVisitor(int api, ClassVisitor cv) {
super(api, cv);
}
/**
* @param version class文件的jdk版本,如50代表JDK 1.7版本
* @param access 类的修饰符,如public、final等
* @param name 类名,但是会以路径的形式来表示,如com.growingio.asmdemo.BlankFragment这个类,
* 最后visit方法中的类名为com/growingio/asmdemo/BlankFragment
* @param signature 泛型信息,如果未定义泛型,则该参数为null
* @param superName 父类名
* @param interfaces 该类实现的接口列表
*/
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
// 如果该类的父类是 android.app.Fragment,说明该类是一个Fragment
mIsFragmentClass = "android/app/Fragment".equals(superName);
if (mIsFragmentClass) {
System.err.println("Find fragment class, it is " + name);
}
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
// 如果该类是Fragment子类,且该方法是onResume,就在该方法中进行代码织入
if (mIsFragmentClass && "onResume".equals(name) && "()V".equals(desc)) {
mHasResumeMethod = true;
return new ResumeMethodVisitor(Opcodes.ASM6, methodVisitor, access, name, desc);
}
return methodVisitor;
}
/**
* 类访问结束的时候调用
*/
@Override
public void visitEnd() {
// 如果该类是Fragment子类,且没有重写onResume方法,那么就添加一个onResume方法
if (mIsFragmentClass && !mHasResumeMethod) {
// 生成方法 public void onResume()
GeneratorAdapter generator = new GeneratorAdapter(ACC_PUBLIC, new Method("onResume", "()V"), null, null, cv);
// 将this对象压入操作栈中,这里其实就是这个Fragment对象
generator.loadThis();
// 调用 super.onResume()
generator.invokeConstructor(Type.getObjectType("android/app/Fragment"), new Method("onResume", "()V"));
// 将this对象压入操作栈中,这里其实就是这个Fragment对象
generator.loadThis();
// 调用静态方法 com.growingio.asmdemo.FragmentAsmInjector#afterFragmentResume(Fragment fragment)
generator.invokeStatic(Type.getObjectType("com/growingio/asmdemo/FragmentAsmInjector"), new Method("afterFragmentResume", "(Landroid/app/Fragment;)V"));
// 调用return并结束该方法
generator.returnValue();
generator.endMethod();
}
super.visitEnd();
}
private static final class ResumeMethodVisitor extends AdviceAdapter {
protected ResumeMethodVisitor(int api, MethodVisitor mv, int access, String name, String desc) {
super(api, mv, access, name, desc);
}
/**
* 方法退出前调用
*/
@Override
protected void onMethodExit(int opcode) {
// 将this对象压入操作栈中,这里其实就是这个Fragment对象
loadThis();
// 调用静态方法 com.growingio.asmdemo.FragmentAsmInjector#afterFragmentResume(Fragment fragment)
invokeStatic(Type.getObjectType("com/growingio/asmdemo/FragmentAsmInjector"), new Method("afterFragmentResume", "(Landroid/app/Fragment;)V"));
super.onMethodExit(opcode);
}
}
}
第3步:用ClassVisitor
遍历每个class文件
我们在之前transform方法中稍稍修改下,将所有class文件经过上述的ClassVisitor
遍历,如下
package com.growingio.autotracker.asm;
import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.internal.pipeline.TransformManager;
import com.android.utils.FileUtils;
import com.google.common.collect.FluentIterable;
import org.apache.commons.io.IOUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Set;
class AutotrackTransform extends Transform {
@Override
public String getName() {
return "autotrackTransform";
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
// 只处理java class文件
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
// 处理整个project,包括依赖
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public boolean isIncremental() {
return false;
}
@Override
public void transform(TransformInvocation transformInvocation) throws IOException {
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
Collection<TransformInput> inputs = transformInvocation.getInputs();
// 遍历所有输入文件
for (TransformInput input : inputs) {
// 遍历所有文件夹
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
FluentIterable<File> allFiles = FileUtils.getAllFiles(directoryInput.getFile());
// 遍历文件夹下面的所有文件
for (File fileInput : allFiles) {
// 获取文件输出的目标文件夹
File outDir = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
outDir.mkdirs();
File fileOut = new File(outDir.getAbsolutePath(), fileInput.getName());
// 如果是java class文件,将进行AOP处理
if (fileInput.getName().endsWith(".class")) {
if (transformClassFile(fileInput, fileOut)) {
System.err.println("Transformed class file " + fileInput.getName() + " successfully");
} else {
System.err.println("Failed to transform class file " + fileInput.getName());
}
}
}
}
// 遍历所有jar包
for (JarInput jarInput : input.getJarInputs()) {
// 获取jar包输出的目标文件
File jarOut = outputProvider.getContentLocation(jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
// 将jar包拷贝到目标文件
FileUtils.copyFile(jarInput.getFile(), jarOut);
}
}
}
private boolean transformClassFile(File from, File to) {
boolean result;
File toParent = to.getParentFile();
toParent.mkdirs();
try (FileInputStream fileInputStream = new FileInputStream(from); FileOutputStream fileOutputStream = new FileOutputStream(to)) {
result = transformClass(fileInputStream, fileOutputStream);
} catch (Exception e) {
e.printStackTrace();
result = false;
}
return result;
}
private boolean transformClass(InputStream from, OutputStream to) {
try {
byte[] bytes = IOUtils.toByteArray(from);
byte[] modifiedClass = visitClassBytes(bytes);
if (modifiedClass != null) {
IOUtils.write(modifiedClass, to);
return true;
} else {
IOUtils.write(bytes, to);
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
private byte[] visitClassBytes(byte[] bytes) {
// 解析该java字节码
ClassReader classReader = new ClassReader(bytes);
// 通ClassWriter修改java字节码
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
// 对java字节码逐个访问
FragmentAopClassVisitor fragmentAopClassVisitor = new FragmentAopClassVisitor(Opcodes.ASM6, classWriter);
classReader.accept(fragmentAopClassVisitor, ClassReader.SKIP_FRAMES | ClassReader.EXPAND_FRAMES);
// 返回修改后的java字节码
return classWriter.toByteArray();
}
}
第4步:将Plugin重新打包并验证
我们重新运行执行task 'publishOfficialPublicationToMavenLocal',将Plugin发布到mavenLocal,并运行APP。 查看Build 窗口下的日志,如下:
> Task :app:transformClassesWithAutotrackTransformForDebug
Transformed class file BuildConfig.class successfully
Transformed class file MainActivity.class successfully
Find fragment class, it is com/growingio/asmdemo/BlankFragment
Transformed class file BlankFragment.class successfully
Transformed class file FragmentAsmInjector.class successfully
查看Logcat窗口日志,如下:
E/FragmentAsmInjector: afterFragmentResume: fragment is BlankFragment{8fafb1e #1 id=0x7f0800a4}
发现织入代码生效了。
总结
我们发现,自定义Gradle Transform,再通过ASM解析Java字节码能解决之前AspectJ如果对应类没有重新方法将无法织入的问题。也可以通过修改字节码解决Lambda表达式的问题。那ASM有什么缺点呢?从目前看来使用该方案是一个相对完美的选择,唯一的缺点就是ASM学习曲线较为陡峭。