Android组件化实战四: APT的介绍与使用

前言

上一篇文章分析了组件化模块交互的两种实现方式,对于全局Map保存目标Activity的路径信息和类对象方案,如果Activity的数量繁多,则需要在主模块app的application中重复执行多次保存操作,既不优雅,又不符合实际开发场景,我们想到的解决之一就是想办法生成一个来完成这个重复枯燥的任务,生成一个类来帮我们找到跳转目标Activity的class对象。好比Butterknife生成一个文件专门完成findViewById的操作一样。这就涉及到注解处理器即apt技术,接下来就了解apt的使用和原理吧

什么是APT

APT全称为Annotation Processing Tool ,翻译过来就是注解处理器,是一种处理注释/注解的工具,它对源代码文件中进行检测找出其中的Annotation,根据注解自动生成代码,如果想要自定义的注解处理器能够正常运行,必须要通过APT工具来进行处理,也可以这样理解,只有通过声明APT工具后,程序在编译期间自定义的注解解释器才能执行。

通俗理解:根据规则,帮我们生成代码、生成类文件

结构体语言

如果对HTML有所了解,就知道它是又element组成的结构体

<html>
    <body>
        <div>
            ...
        </div>
    </body>
</html>

而对于java源文件来说,它同样是一种结构体语言,我们不可能把类名写在包名之上,也不可能把属性写在类名之上,这是规则,包名、类名、成员属性、成员方法,与之相对应的就是程序元素/节点

package com.example.modular_apt;     // PackageElement 包元素/节点

public class User {                  // TypeElement类元素/节点
    private String name;             // VariableElement属性元素/节点

    public User() {                  // ExecutableElement方法元素/节点
    }
    private void setName(String name) {
        this.name = name;
    }
}

Element程序元素

  • PackageElement: 表示一个包程序元素。提供对有关包及其成员的信息的访问
  • TypeElement: 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问
  • VariableElement: 表示一个字段、enum常量、方法或构造方法参数、局部变量或异常参数
  • ExecutableElement: 表示某个类或接口的方法、构造方法或初始化程序(静态或实例)

需要掌握的API

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lWJcTISk-1592439507441)(C:\Users\dell\Pictures\Camera Roll\组件化\APT涉及的api.png)]

创建工程实战

构建app module, 在该module中有MainActivity、OrderActivity、PersonalActivity三个Activity,通过apt技术即注解处理器,根据对应的注解,生成类文件,获取对应Activity的class对象。这样即使在不同的模块,只要拿到了目标Activity的class对象,就可以进行跳转了。

构建java library annotation 在该java库中有一个类注解ARouter,注解处理器库就是根据这个注解的参数,获取类名。

@Target(ElementType.TYPE) // 该注解作用在类之上
@Retention(RetentionPolicy.CLASS) // 要在编译时进行一些预处理操作。注解会在class文件中存在
public @interface ARouter {
    // 详细路由路径(必填),如:"/app/MainActivity"
    String path();

    // 路由组名(选填,如果开发者不填写,可以从path中截取出来)
    String group() default "";
}

构建java library compiler,在该java库中也只有一个核心类ARouterProcessor,即注解处理类。核心方法就是process,在下面代码中,该方法的主要逻辑就是,遍历所有使用了类注解ARouter的类集合set,根据类节点(信息)获取包节点(信息),然后根据包节点获取类名,如果等于注解传递的参数中的类名,就生成对应的文件,具体逻辑如下:

// AutoService则是固定的写法,加个注解即可
// 通过auto-service中的@AutoService可以自动生成AutoService注解处理器,用来注册
// 用来生成 META-INF/services/javax.annotation.processing.Processor 文件
@AutoService(Processor.class)
// 允许/支持的注解类型,让注解处理器处理(新增annotation module)
@SupportedAnnotationTypes({"com.example.annotation.ARouter"})
// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 注解处理器接收的参数
@SupportedOptions("content")
public class ARouterProcessor extends AbstractProcessor {

    // 操作Element工具类 (类、函数、属性都是Element)
    private Elements elementUtils;

    // type(类信息)工具类,包含用于操作TypeMirror的工具方法
    private Types typeUtils;

    // Messager用来报告错误,警告和其他提示信息
    private Messager messager;

    // 文件生成器 类/资源,Filter用来创建新的源文件,class文件以及辅助文件
    private Filer filer;

    // 该方法主要用于一些初始化的操作,通过该方法的参数ProcessingEnvironment可以获取一些列有用的工具类
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // 父类受保护属性,可以直接拿来使用。
        // 其实就是init方法的参数ProcessingEnvironment
        // processingEnv.getMessager(); //参考源码64行
        elementUtils = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();

        // 通过ProcessingEnvironment去获取build.gradle(app module)传过来的参数
        String content = processingEnvironment.getOptions().get("content");
        // 有坑:Diagnostic.Kind.ERROR,异常会自动结束,不像安卓中Log.e那么好使
        messager.printMessage(Diagnostic.Kind.NOTE, content);
    }

    /**
     * 相当于main函数,开始处理注解
     * 注解处理器的核心方法,处理具体的注解,生成Java文件
     *
     * @param set              使用了支持处理注解的节点集合(类 上面写了注解)
     * @param roundEnvironment 当前或是之前的运行环境,可以通过该对象查找找到的注解。
     * @return true            表示后续处理器不会再处理(已经处理完成)
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set.isEmpty()) return false;

        // 获取所有带ARouter注解的 类节点
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ARouter.class);
        // 遍历所有类节点
        for (Element element : elements) {
            // 通过类节点获取包节点(也就是包名:com.example.xxx)
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
            // 获取简单类名(不带包名)
            String className = element.getSimpleName().toString();
            messager.printMessage(Diagnostic.Kind.NOTE, "被注解的类有:" + className);
            // 最终想生成的类文件名
            String finalClassName = className + "$$ARouter";

            // 公开课写法,也是EventBus写法(https://github.com/greenrobot/EventBus)
            try {
                // 创建一个新的源文件(Class),并返回一个对象以允许写入它
                JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + finalClassName);
                // 定义Writer对象,开启写入
                Writer writer = sourceFile.openWriter();
                // 设置包名
                writer.write("package " + packageName + ";\n");

                writer.write("public class " + finalClassName + " {\n");

                writer.write("public static Class<?> findTargetClass(String path) {\n");

                // 获取类之上@ARouter注解的path值
                ARouter aRouter = element.getAnnotation(ARouter.class);

                writer.write("if (path.equalsIgnoreCase(\"" + aRouter.path() + "\")) {\n");

                writer.write("return " + className + ".class;\n}\n");

                writer.write("return null;\n");

                writer.write("}\n}");

                // 最后结束别忘了
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return true;
    }
}

生成对应的java文件如下:

public class MainActivity$$ARouter {
    public static Class<?> findTargetClass(String path) {
        if (path.equalsIgnoreCase("/app/MainActivity")) {
            return MainActivity.class;
        }
        return null;
    }
}

同理,只要在OrderActivity和PersonalActivity分别添加对应的类注解,@ARouter(path = “/app/OrderActivity”)和@ARouter(path = “/app/PersonalActivity”),也会生成对应的java文件OrderActivity $ $ ARouter和PersonalActivity$$ ARouter。

此时从MainActivity跳转OrderActivity的逻辑如下:

Class<?> targetClass = OrderActivity$$ARouter.findTargetClass("/app/OrderActivity");
startActivity(new Intent(this, targetClass));

表明上看这是多此一举,直接用下面的方式跳转不香吗呢?原因是如果目标Activity在不同的子模块,即子模块相互没有依赖关系,是无法进行下面的操作的。这里为了便于演示,所有将三个Activity放在一个模块中了。

startActivity(new Intent(this, OrderActivity.class));

注意事项

  • 注解处理器依赖
//注解处理器依赖
compileOnly'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
  • 乱码问题
// java控制台输出中文乱码
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
  • 传参问题
defaultConfig {
    ...
    //在gradle文件中配置选项参数值(用于APT传参接收)
    //切记:必须写在defaultConfig节点下
    javaCompileOptions {
        annotationProcessorOptions {
            arguments = [content: 'hello apt']
        }
    }
}

代码链接:https://github.com/xpf-android/Modular_APT

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章