白话 Android AOP (二)

1,032 阅读9分钟

《白话 Android AOP (一)》中,我们从 Activity 的 onPause/onResume 日志打印需求出发延伸出了 AGP 的 Transform API ,从而借助 Transform API 以 AOP 的方式实现了同样的需求。之所以能够顺利的使用 AOP 的思想解决我们的需求,其实有一个隐形的标记(或者规则),那就是所有 Activity 的 onPause/onResume 方法都打印日志,这就是我们需要的切面。
那如果我们的需求里没有现成的标记(或者规则、切面),还能使用 AOP 的思想去实现吗?能!没标记我们加标记呗!这个标记就是注解 Annotation

Java Annotation

Annotation 注解,是元数据的一种形式,它提供有关程序的数据,但这些数据不是程序本身的一部分。注解对它们注释的代码的操作没有直接影响。 注解有许多用途,其中包括:

  • 为编译器提供信息- 编译器可以使用注解来检测错误或提示警告。
  • 编译时、部署时处理- 软件工具可以处理注解信息以生成代码、XML文件等等。
  • 运行时处理- 一些类型的注解在运行时仍然可以被检测。

其实在 Android 开发中,我们经常用到注解,比如 @Override @Nullable,我们先瞥一眼它长什么样:

// Nullable.java
@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})
public @interface Nullable {
}

元注解

奇怪,上面的 Nullable 注解定义代码里 @Documented @Retention @Target 又是什么,也是注解吗?它们的确是注解,这类作用于注解的注解,叫做元注解。
java.lang.annotation 包中定义了几种元注解类型。 @Retention 该注解指定如何存储被其标记的注解:

RetentionPolicy.SOURCE – 被标记的注解仅保留在源代码级别,并被编译器忽略。
RetentionPolicy.CLASS – 被标记的注解在编译时被编译器保留,但被Java虚拟机(JVM)忽略。
RetentionPolicy.RUNTIME – 被标记的注解被JVM保留,以便运行时环境可以使用。

@Documented 该注解表明,无论何时使用被其标记的注解,都应该使用 Javadoc 工具对这些元素进行文档化。(默认情况下,注解不包含在 Javadoc 中。)

@Target 该注解标记另一个注解,以限制被标记的注解可以应用于哪种类型的 Java 元素。目标注解支持以下元素类型

ElementType.ANNOTATION_TYPE 可应用于注解类型。
ElementType.CONSTRUCTOR 可应用于构造函数。
ElementType.FIELD 可应用于字段或者属性。
ElementType.LOCAL_VARIABLE 可应用于局部变量。
ElementType.METHOD 可应用于函数方法。
ElementType.PACKAGE 可应用于包声明。
ElementType.PARAMETER 可应用于方法参数。
ElementType.TYPE 可以应用于类的任何元素。

@Inherited 该注解表示可以从父类继承注解类型(默认情况不继承)。当查询注解类型而当前类没有对应的注解时,将查询它的父类以获取相应注解。此注解仅应用于类声明。

Transform With Annotation

搞清楚了注解的具体概念,我们把需求改成打印被注解的函数名称、参数名称以及参数取值。首先我们需要定义注解,然后对需要打印日志的函数添加注解标记,然后通过 Transform API 识别注解并使用 ASM 添加日志打印代码。

定义注解

我们的注解被用到了两个地方:一个是被注解函数所在的 Module,一个是处理注解的 Gradle 插件 Module 。所以比较合理的做法是把注解作为一个单独的 Java Library Module,然后让这两个 Module 依赖注解模块。新建一个 Java Library 类型的模块,然后添加注解代码:

// 我们的 Transform API 处理的是 .class 文件,所以我们需要注解被编译器保留
@Retention(RetentionPolicy.CLASS)
// 我们的注解只能应用于方法函数
@Target({ElementType.METHOD})
public @interface MethodLoggable {
}

使用注解标记方法

app 模块添加对上述 annotation 模块的依赖,然后对需要打印日志的方法添加注解标记:

@MethodLoggable
private void testMethod(String name, int age) {
    int i = 10;
    int j = age + i;
}

处理注解

先创建 Gradle Plugin Module,不知道怎么操作的请看 Android Gradle 插件开发入门指南(一)。然后和白话 Android AOP (一) 一样,添加 ASM 依赖、注册 Transform、找到所有的 .class 文件,然后分两步实现需求。

  1. 解析函数参数

我们先对比着看下一个函数的 Java 代码和字节码代码: 未标题-1副本.png LOCALVARIABLE 表示和该函数关联的本地变量,这里面有函数参数的名称。这些信息竟然在函数 return 语句后面,我们就没办法解析函数参数的同时去添加日志打印逻辑。我这里用了一个比较笨的办法,每一个 .class 文件解析两边,第一遍获取方法参数信息,第二遍添加日志打印逻辑。实际生产中使用 AspectJ 能比较容易实现上述需求,由于我对 AspectJ 不熟悉,所以采用了这么蠢的做法,大家千万别这么干。其实也不必这么干,因为 Jake Wharton 大神已经帮我们实现好了 github.com/JakeWharton…。 方法参数解析的主要代码,完整代码见 LoggableMethodTransform.jav LoggableMethodParser.java

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
                                 String[] exceptions) {
    // 参数名称只能从本地变量里获取,但是本地变量不仅仅包含函数参数。
    // 我们可以通过参数 Type 数组得知具体有多少个参数
    Type[] types = Type.getArgumentTypes(descriptor);
    MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
    // 非静态方法第一个参数是 this
    boolean staticMethod = (access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC;
    return new ArgumentsReader(mv, staticMethod, name, types);
}

class ArgumentsReader extends MethodVisitor {
    ......
    @Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        // 遍历到函数注解,我们在这里可以筛选出被我们比较的函数
        if (METHOD_LOGGABLE_DESC.equals(descriptor)) {
            loggableMethod = true;
        }
        return super.visitAnnotation(descriptor, visible);
    }
    
    @Override
    public void visitLocalVariable(String name, String descriptor, String signature,
                                   Label start, Label end, int index) {
        super.visitLocalVariable(name, descriptor, signature, start, end, index);
        if (loggableMethod && argumentNames != null) {
            // 只收集被标记的函数参数名称
            if (index >= 0 && index < argumentCount) {
                argumentNames[index] = name;
            }
        }
    }
}
  1. 向被注解的函数里插入日志打印代码

日志插入主要代码,完整代码见 LoggableMethodTransform.java LoggableMethodPrinter.java

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
                                 String[] exceptions) {
    MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
    if (methodToArgumentArray != null && methodToArgumentArray.containsKey(name)) {
        boolean staticMethod = (access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC;
        int offset = staticMethod ? 0 : 1;
        Type[] types = Type.getArgumentTypes(descriptor);
        String[] argumentNames = methodToArgumentArray.get(name);
        final int argumentsCount = argumentNames.length;
        mv.visitLdcInsn("lenebf");
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        mv.visitInsn(Opcodes.DUP);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        mv.visitLdcInsn("Invoke method " + name + "(");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        String argumentName;
        String argumentTypeDescriptor;
        for (int index = 0; index < argumentsCount; index++) {
            argumentName = argumentNames[index];
            if ("this".equals(argumentName)) {
                // 排除掉 this 参数
                continue;
            }
            // 参数名称
            mv.visitLdcInsn(argumentName + ": ");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            // 参数取值
            argumentTypeDescriptor = types[index - offset].getDescriptor();
            // 这句代码的意思:读取第 index 个参数的值
            mv.visitVarInsn(Opcodes.ILOAD, index);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(" + argumentTypeDescriptor + ")Ljava/lang/StringBuilder;", false);
            if (index != argumentsCount - 1) {
                // 非最后一项插入 , 隔开参数
                mv.visitLdcInsn(", ");
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            }
        }
        mv.visitLdcInsn(")");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
    }
    return mv;
}

验证插件效果

  1. 查看 Transform 生成的代码

image.png

  1. 查看代码运行结果

image.png 很棒,完全符合我们的预期。

Annotation Processing Tool

Annotation 是不是特别有用,以至于它有专有的处理工具,那就是 Annotation Processing Tool (APT)。APT 基于指定源文件中存在的注解,查找并执行注解处理器 (Annotation Processor),注解处理器根据注解标记提供的信息修改源码或者生成新的源码文件,被修改过的源码文件或者新生成的源码文件都能被编译,从而达到修改原有逻辑或者添加新逻辑的目的。我们经常使用的 butterknife, ARouter 都是使用 APT 技术的杰作。
套路依旧,我们把需求改成可以在被注解的类里获取该类的编译时间。

JavaPoet

根据前面的简介,我们知道 APT 是通过 Annotation Processor 来处理注解的,而 Annotation Processor 面向的是源码即 .java,所以 ASM 就不适用了。所以我们需要新的工具 JavaPoet。JavaPoet 由 Square 公司出产,是一组用来生成 .java 源码文件的 API,详细用法见 github.com/square/java…。同时 Square 还出产了我们常用的 OkHttp, Retrofit,这公司真厉害!

定义注解

新建一个 Java Library Module,然后定义我们的注解:

// 注解处理器处理的是 .java 文件,我们只需要注解在源码级别被保留
@Retention(RetentionPolicy.SOURCE)
// 我们的注解只能应用于类
@Target({ElementType.TYPE})
public @interface KeepBuildTime {
}

Annotation Processor

有了代码生成工具,我们来实现注解处理器。基本逻辑很简单,新建一个 Java Library Module,添加对 JavaPoet 和上述注解 Module 的依赖,然后定义一个继承于 AbstractProcessor 的类,实现关键方法即可。

@AutoService(Processor.class)
public class BuildTimeProcessor extends AbstractProcessor {

    private Elements elementUtils;
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        // 获取元素处理工具类
        elementUtils = processingEnvironment.getElementUtils();
        // 用于创建新源文件、类文件或辅助文件的文件管理器
        filer = processingEnvironment.getFiler();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 注解处理器需要处理哪些注解
        return Collections.singleton(KeepBuildTime.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 处理注解
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(KeepBuildTime.class);
        for (Element element : elements) {
            generateBTCLass((TypeElement) element);
        }
        return true;
    }

    private void generateBTCLass(TypeElement element) {
        // 被注解的类名 + "_BT" 作为工具类的类名
        String btClassName = element.getSimpleName().toString() + "_BT";
        // 被注解的类包名
        String packageName = elementUtils.getPackageOf(element).toString();
        // 生成获取编译时间的工具类
        TypeSpec btClass = TypeSpec.classBuilder(btClassName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                // 生成获取编译时间的方法
                .addMethod(generateGetBuildTimeMethod())
                .build();
        JavaFile javaFile = JavaFile.builder(packageName, btClass).build();
        try {
            // 输出生成的新类
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private MethodSpec generateGetBuildTimeMethod() {
        long buildTime = System.currentTimeMillis();
        // 方法名为 getBuildTime
        return MethodSpec.methodBuilder("getBuildTime")
                // 方法为静态公开方法
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                // 返回 long 型
                .returns(long.class)
                .addStatement("return " + buildTime + "L")
                .build();
    }
}

AutoService

上面的代码中 @AutoService(Processor.class) 是什么东西呢?为了使注解处理器正常工作,和开发 Gradle Plugin 类似,需要一个配置文件类描述注解处理器的实现,具体操作如下:

  1. 在注解处理器 Module 的 main 目录下新建 resources/META-INF/services 文件夹;
  2. 在 resources/META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
  3. 在 javax.annotation.processing.Processor 文件写入注解处理器的类全名,我们例子中就是 com.lenebf.android.buildtime_processor.BuildTimeProcessor;

是不是特别啰嗦,Gradle 插件开发时有 Gradle Plugin Development Plugin 辅助我们生成配置文件,注解处理器开发同样有老大哥帮我们开发了相应的工具,那就是来自 Google 的 AutoService
官方描述(机翻),Java 注解处理器和其他系统使用 java.util.ServiceLoader 来注册使用 META-INF 元数据的已知类型的实现。 但是,开发人员很容易忘记更新或正确指定服务描述符。
对于使用 @AutoService 注释的任何类,AutoService会为开发人员生成此元数据,避免输入错误,防止重构错误等。

访问生成的工具类

前面注解处理器根据注解提供的信息生成了 "_BT" 结尾的工具类,工具类里有名为 "getBuildTime" 的静态方法,我们访问该方法就能获取对应类的编译时间了。不同的类,这一套解析逻辑是完全相同,所以比较好的做法是新建一个工具 Module 专门获取对应类的编译时间,我们新建一个名为 buildtime 的 Java Library Module,然后实现读取逻辑:

public class BuildTime {

    public static long get(Object object) {
        try {
            String btClassName = object.getClass().getCanonicalName() + "_BT";
            Class<?> btClass = Class.forName(btClassName);
            Method getBuildTimeMethod = btClass.getMethod("getBuildTime");
            return (long) getBuildTimeMethod.invoke(object);
        } catch (Throwable throwable) {
            return -1L;
        }
    }
}

验证效果

现在让 app moudle 依赖 buildtime_annotation, buildtime, buildtime_processor:

// 注解定义 module
implementation project(path: ':buildtime_annotation')
// 工具类 module
implementation project(path: ':buildtime')
// 注解处理器的以来引入方法不同
annotationProcessor project(path: ':buildtime_processor')

然后给 app 的 MainActivity 添加 @KeepBuildTime 注解,并日志输出编译时间:

@KeepBuildTime
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d("lenebf", "The build time is " + BuildTime.get(this));
    }
}

然后看看我们生成的工具类,以及实际的日志输出: image.png image.png 一如既往的完美!

总结

实践 AOP 方法论时,如果有明显的规则(标记)可以提取切面,我们可以直接处理切面。如果没有明显的规则(标记)提取切面,我们可以通过添加注解的方式创建切面,通过添加注解创建的切面既可以使用 Transform + ASM 来实现代码逻辑,也可以通过 APT + JavaPoet 来处理。
例子代码地址:github.com/lenebf/Andr…