Android AOP方案(二)——ASM

3,905 阅读9分钟

介绍

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:只处理当前的module
  • SUB_PROJECTS:只处理子module
  • EXTERNAL_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步:定义织入代码

我们定义一个类和一个静态方法,待会在FragmentonResume方法中执行该方法,该类在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学习曲线较为陡峭。