Android模块化点滴-APT

306 阅读3分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

APT(Annotation Processing Tool)

是一种处理注释的工具,它对源代码文件进行检测,找出其中的Annotation,根据注解自动生成代码,如果想要自定义的注解处理器能够正常运行,必须要通过APT工具来进行处理。也可以这样理解,只有通过声明APT工具后,程序在编译期间自定义注解解释器才能运行。简而言之:根据规则,生成代码或类文件。

文件结构

package com.loveqrc.aptdemo //PackageElement  包元素/节点

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {  //TypeElement 类元素/节点
    val userName = "Rc在努力" //VariableElement 属性元素/节点

    override fun onCreate(savedInstanceState: Bundle?) {  //ExecutableElement 方法元素/节点
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
  • PackageElement 表示一个包程序元素。提供对有关包及其成员的信息的访问。
  • ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例)。
  • TypeElement表示一个类或接口程序元素。提供对有关类型及其成员的信息访问。
  • VariableElement表示一个字段、enum常量、方法或构造方法参数、局部变量。

常用API

方法名方法解析
getEnclosedElements()返回该元素直接包含的子元素
getEnclosingElement()返回包含该element的父element,与getEnclosedElements() 效果相反
getSimpleName()获取名字,不带包名
getQualifiedName()获取全名,如果是类的话,包含完整的包名路径

创建Annotaion

新建java lib ,用于存放Annotaion

image.png

创建ARouter注解

@Target(AnnotationTarget.CLASS) // 该注解作用在类之上
@kotlin.annotation.Retention(AnnotationRetention.BINARY) // 要在编译时进行一些预处理操作。注解会在class文件中存在
annotation class ARouter(
    // 详细路由路径(必填),如:"/app/MainActivity"
    val path: String,
    // 路由组名(选填,如果开发者不填写,可以从path中截取出来)
    val group: String = ""
)

target表明注解在哪个位置

  • @Target(ElementType.TYPE) // 接口、类、枚举、注解
  • @Target(ElementType.FIELD) // 属性、枚举的常量
  • @Target(ElementType.METHOD) // 方法
  • @Target(ElementType.PARAMETER) // 方法参数
  • @Target(ElementType.CONSTRUCTOR) // 构造函数
  • @Target(ElementType.LOCAL_VARIABLE)// 局部变量
  • @Target(ElementType.ANNOTATION_TYPE)// 该注解使用在另一个注解上
  • @Target(ElementType.PACKAGE) // 包

Retention表示注解的生命周期

生命周期:SOURCE < CLASS(BINARY) < RUNTIME
1、一般如果需要在运行时去动态获取注解信息,用RUNTIME注解,会在class字节码文件中存在,jvm加载时可以通过反射获取到该注解的内容
2、要在编译时进行一些预处理操作,如ButterKnife,用CLASS注解。注解会在class文件中存在,但是在运行时会被丢弃
3、做一些检查性的操作,如@Override,用SOURCE源码注解。注解仅存在源码级别,在编译的时候丢弃该注解

创建注解处理器

新建java lib ,用于处理Annotaion

添加依赖

plugins {
    id 'java-library'
    id 'kotlin'
    id 'kotlin-kapt'
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //注解处理器
    kapt 'com.google.auto.service:auto-service:1.0-rc6'
    api 'com.google.auto.service:auto-service:1.0-rc6'

    // 引入annotation,让注解处理器-处理注解
    implementation project(':annotation')
}
// java控制台输出中文乱码,但是kotlin并不生效......
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
java {
    sourceCompatibility = JavaVersion.VERSION_1_7
    targetCompatibility = JavaVersion.VERSION_1_7
}

创建ARouterProcessor集成AbstractProcessor

// AutoService则是固定的写法,加个注解即可
// 通过auto-service中的@AutoService可以自动生成AutoService注解处理器,用来注册
// 用来生成 META-INF/services/javax.annotation.processing.Processor 文件
@AutoService(Processor::class)
// 允许/支持的注解类型,让注解处理器处理(新增annotation module)
@SupportedAnnotationTypes("com.loveqrc.annotation.ARouter")
// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 注解处理器接收的参数
@SupportedOptions("content")
class ARouterProcessor : AbstractProcessor() {
    // 操作Element工具类 (类、函数、属性都是Element)
    lateinit var elementUtils: Elements

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

    // Messager用来报告错误,警告和其他提示信息
    lateinit var messager: Messager

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


    override fun init(processingEnv: ProcessingEnvironment) {
        super.init(processingEnv)

        elementUtils = processingEnv.elementUtils;
        messager = processingEnv.messager;
        filer = processingEnv.filer;
        messager.printMessage(Diagnostic.Kind.NOTE, "Rc woooo");
        val content: String? = processingEnv.options["content"]
        messager.printMessage(Diagnostic.Kind.NOTE, content);

    }

    /**
     * 相当于main函数,开始处理注解
     * 注解处理器的核心方法,处理具体的注解,生成Java文件
     *
     * @param annotations             
使用了支持处理注解的节点集合(类 上面写了注解),因为注解处理器可以处理多个注解,所以个这里返回的时集合
     * @param roundEnv 当前或是之前的运行环境,可以通过该对象查找找到的注解。
     * @return true 表示后续处理器不会再处理(已经处理完成)
     */
    override fun process(
        annotations: MutableSet<out TypeElement>,
        roundEnv: RoundEnvironment
    ): Boolean {
        if (annotations.isEmpty()) return false;

        // 获取所有带ARouter注解的 类节点
        val elements: Set<Element> = roundEnv.getElementsAnnotatedWith(
            ARouter::class.java
        )
        elements.forEach {
            //获取包名
            val packageName = elementUtils.getPackageOf(it).qualifiedName.toString();
            messager.printMessage(Diagnostic.Kind.NOTE, packageName);

            // 获取简单类名
            val className: String = it.simpleName.toString()
            //被注解的类
            messager.printMessage(Diagnostic.Kind.NOTE, className);

            // 定义最终想生成的类文件名
            val finalClassName = className +"ARouter"

            try {
                // 创建一个新的源文件(Class),并返回一个对象以允许写入它
                val sourceFile = filer.createSourceFile("$packageName.$finalClassName")
                // 定义Writer对象,开启写入
                val 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值
                val aRouter: ARouter = it.getAnnotation(ARouter::class.java)
                writer.write(
                    """
            if (path.equals("${aRouter.path}")) {
            
            """.trimIndent()
                )
                writer.write("return $className.class;\n}\n")
                writer.write("return null;\n")
                writer.write("}\n}")

                // 最后关闭流
                writer.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }


        }

        return true
    }
}

首先使用@AutoService注册ARouterProcessor,类似Activity要在manifest中注册一样,然后绑定它要处理的注解,一个注解处理器可以处理多个不同的注解,这就是为什么ARouterProcessorprocess会返回MutableSet<out TypeElement>原因,随后绑定编译的版本和接受的参数。传入的参数在引入的工程中配置,如下所示

android {
    defaultConfig {
        kapt {
            arguments {
                arg("content", project.getName())
            }
        }
    }
    }

使用注解处理器

创建注解和注解处理器之后,就可以检验一下是否正确了,首先添加依赖

// 依赖注解
implementation project(':annotation')
// 依赖注解处理器
kapt project(':compiler')

然后在Activity中添加注解

@ARouter(path = "app/MainActivity")
class MainActivity : AppCompatActivity() {
}

然后重新编译,在build文件夹下能看到注解处理器生成的文件

image.png

生成的文件时这样的

package com.loveqrc.aptdemo;

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

生成文件后,就能通过MainActivityARouter查找对应的class了。从上面可以知道 通过APT生成想要的文件,能减少开发工作量,利用的好能大大提供开发效率。