APT入门

作者: 编程的猫 | 来源:发表于2021-12-25 18:06 被阅读0次

    首先看一下一张抽象的Android项目编译流程图


    Android工程编译流程.png

    APT(Annotation Processing Tool) 注解处理工具,作为编译中动态处理技术,作用在Android代码编译流程中class字节码被编译转换成dex字节码文件之前的这一环节。

    之前有写过Java APT入门的一篇文章,这里介绍Kotlin KAPT,这里暂时不做实战介绍,后续有时间会补上(APT、KAPT、KSP是一个系列的文章)

    本文使用KAPT最终生成下列文件代码为示例介绍:

    package com.icat.myapt.kotlin
    
    import kotlin.Boolean
    import kotlin.Unit
    
    public class MainActivity_BindView {
      public final fun bindView(activity: MainActivity): Unit {
        activity.testTv = activity.window.decorView.findViewById<android.widget.TextView>(2131231134)
      }
    
      public companion object {
        public const val LOVE: Boolean = true
      }
    }
    
    大致分为以下步骤
    • 新建Android工程
    • 新建名为AnnotationLib的JavaLibarry,用于编写需要使用到的注解
    • 新建名为AnnotationProcessor的JavaLibrary,用于处理编译过程中的目标注解,生成Kotlin文件

    完成以上步骤后的工程结构如下:


    image.png

    三个module的依赖关系如下图:


    image.png
    App依赖AnnotationLib和AnnotationProcessor,AnnotationProcessor依赖AnnotationLib。
    在AnnotationProcessor的build.gradle中配置一下依赖资源:
    plugins {
        id 'java-library'
        id 'kotlin'
        id 'kotlin-kapt'
    }
    
    java {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    
    dependencies {
        // 依赖注解库
        implementation project(path: ':AnnotationLib')
        // 用于自动注册注解
        implementation 'com.google.auto.service:auto-service:1.0-rc6'
        kapt 'com.google.auto.service:auto-service:1.0-rc6'
        // 用于生成kotlin文件(1.7.2版本最低支持JDK1.8)
        implementation 'com.squareup:kotlinpoet:1.7.2'
    }
    

    依赖AnnotationLib后续会引用其中的自定义注解,auto-service自动注册注解的处理框架,kotlinpoet用来调用它的api生成lotlin代码

    在AnnotationLib中新建ClassPath和ViewId编译时注解

    @Target(AnnotationTarget.CLASS)
    // 编译时注解,在反射时不可见
    @Retention(AnnotationRetention.BINARY)
    annotation class ClassPath()
    
    @Target(AnnotationTarget.FIELD)
    // 编译时注解,在反射时不可见
    @Retention(AnnotationRetention.BINARY)
    annotation class ViewId(val viewId: Int)
    

    在App的MainActivity中添加ClassPath和ViewId注解

    @ClassPath
    class MainActivity : AppCompatActivity() {
    
        @AutoWire
        @ViewId(R.id.testTv)
        var testTv: TextView? = null
    

    然后在AnnotationProcessor自定义一个FindViewProcessor重写以下方法

    @AutoService(Processor::class)
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    class FindViewProcessor : AbstractProcessor() {
    
        /**
         * 用于操作元素的实用方法的实现
         */
        private lateinit var mElementUtil: Elements
    
        /**
         * 用于对类型进行操作的实用程序方法的实现
         */
        private lateinit var mTypeUtil: Types
    
    
        /**
         * 添加支持的注解,在编译过程中会对这些注解进行过滤
         */
        override fun getSupportedAnnotationTypes(): MutableSet<String> {
            val supportedAnnotationTypes = mutableSetOf<String>()
            // 添加支持的注解
            supportedAnnotationTypes.add(ClassPath::class.java.canonicalName)
            supportedAnnotationTypes.add(ViewId::class.java.canonicalName)
            return supportedAnnotationTypes
        }
    
        override fun init(processingEnv: ProcessingEnvironment?) {
            super.init(processingEnv)
            println("==【Kt】=========== 【进入init方法】 =============")
            processingEnv?.let {
                mElementUtil = it.elementUtils
                mTypeUtil = it.typeUtils
            }
    
        }
    
        override fun process(annotations: MutableSet<out TypeElement>?, roundEnvironment: RoundEnvironment?): Boolean {
            println("==【Kt】=========== 【进入process方法】 =============")
      
            return false
        }
    

    然后编译(Build--->ReBuild),控制台切换到Build,观察Builder打印的信息,如果FindViewProcessor被编译到了就会打印


    image.png

    下边编写生成目标文件的代码(MainActivity_BindView),主要分为一下6步:

    • Step1: 根据指定注解找到被注解标记的类
    • Step2: 遍历每一个被注解标记的类
    • Step3: 构造类里边的方法
    • Step4: 构造类
    • Step5: 构造文件File
    • Step6: 写入文件
    下边列出主要的语法
    生成属性
    PropertySpec.builder("变量名称","类型")
                .mutable("是否是可变变量")
                .initializer("初始化值")
                .addModifiers("添加限定符")
                .build()
    
    生成方法
     FunSpec.builder("方法名称")
                .addParameter("添加方法参数")
                .addStatement("添加方法体语句")
                .addAnnotation("添加注解")
                .addModifiers("添加方法限制符,例如private、final、const...")
                .returns("方法返回类型")
                .build()
    
    生成companion object代码块(注意:一个类里边只能有一个静态代码块)
    TypeSpec.companionObjectBuilder()
                .addProperties("静态变量集合")
                .addFunction("往companion object中添加方法")
                .build()
    
    生成类
    TypeSpec.classBuilder("类名")
                .addType("添加类成员:方法、内部类、接口、属性、对象等")
                .addFunction("添加方法")
                .build()
    
    生成文件
    FileSpec.builder("包名","文件名")
                .addType("文件里边的内容对象:方法、类、接口、变量、常量等")
                .build()
    
    最后写入文件
     private fun FileSpec.writeFile() {
            val kaptKotlinGeneratedDir = processingEnv.options["kapt.kotlin.generated"]
            kaptKotlinGeneratedDir?.let {
                val outputFile = File(it).apply {
                    mkdirs()
                }
                println("====> 文件数量  -> 路径:${outputFile.toPath()}    it:$it")
                writeTo(outputFile.toPath())
            }
        }
    

    可以针对属性变量、方法、类、静态代码块、接口、文件等做一些封装,下边贴出FindViewProcessor:

    package com.icat.annoteprocessor.processors
    
    import com.google.auto.service.AutoService
    import com.icat.annotationlib.annotations.*
    import com.icat.annoteprocessor.Logger
    import com.icat.annoteprocessor.sentence.*
    import com.squareup.kotlinpoet.*
    import com.squareup.kotlinpoet.jvm.jvmDefault
    import org.checkerframework.checker.units.qual.A
    import javax.annotation.processing.*
    import javax.lang.model.SourceVersion
    import javax.lang.model.util.Elements
    import javax.lang.model.util.Types
    import java.util.*
    import javax.lang.model.element.*
    import javax.lang.model.element.TypeElement
    import java.io.File
    
    /**
     * @author: PengQun
     * @since: 12/22/21
     * @Description: 过滤生成findView文件的注解处理器
     */
    @AutoService(Processor::class)
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    class FindViewProcessor : AbstractProcessor() {
    
        private lateinit var mLogger: Logger
    
        /**
         * 用于操作元素的实用方法的实现
         */
        private lateinit var mElementUtil: Elements
    
        /**
         * 用于对类型进行操作的实用程序方法的实现
         */
        private lateinit var mTypeUtil: Types
    
        /**
         * 用于创建新的源,类或辅助文件的文件管理器
         */
        private lateinit var mFiler: Filer
    
        /**
         * 用于生成方法
         */
        private lateinit var mFunSpecUtil: SentenceFunSpecUtil
    
        /**
         * 用于生成属性
         */
        private lateinit var mPropertiesSpecUtil: SentencePropertiesUtil
    
        /**
         * 用于生成类
         */
        private lateinit var mClassSpecUtil: SentenceClassSpecUtil
    
        /**
         * 用于缓存TypeSpec
         */
        private val typeSpecSet = mutableSetOf<TypeSpec>()
        /**
         * 用于缓存FunSpec
         */
        private val funSpecSet = mutableSetOf<FunSpec>()
        /**
         * 用于缓存propertySpec
         */
        private val propertySpecSet = mutableSetOf<PropertySpec>()
        /**
         * 用于缓存静态propertySpec
         */
        private val propertySpecCompanionSet = mutableSetOf<PropertySpec>()
        /**
         * 用于缓存FileSpec
         */
        private val fileSpecSet = mutableSetOf<FileSpec>()
    
        /**
         * 添加支持的注解,在编译过程中会对这些注解进行过滤
         */
        override fun getSupportedAnnotationTypes(): MutableSet<String> {
            val supportedAnnotationTypes = mutableSetOf<String>()
            // 添加支持的注解
            supportedAnnotationTypes.add(ClassPath::class.java.canonicalName)
            supportedAnnotationTypes.add(ViewId::class.java.canonicalName)
            return supportedAnnotationTypes
        }
    
        override fun init(processingEnv: ProcessingEnvironment?) {
            super.init(processingEnv)
            println("==【Kt】=========== 【进入init方法】 =============")
            processingEnv?.let {
                mLogger = Logger(it.messager)
    
                mElementUtil = it.elementUtils
    
                mTypeUtil = it.typeUtils
    
                mFiler = it.filer
    
                mFunSpecUtil = SentenceFunSpecUtil()
                mPropertiesSpecUtil = SentencePropertiesUtil()
                mClassSpecUtil = SentenceClassSpecUtil()
            }
    
        }
    
        override fun process(annotations: MutableSet<out TypeElement>?, roundEnvironment: RoundEnvironment?): Boolean {
            println("==【Kt】=========== 【进入process方法】 =============")
            annotations?.let {
                annotations.forEach { typeElement ->
                    val qualifiedName = typeElement.qualifiedName.toString()
                    val canonicalName = ClassPath::class.java.canonicalName
                    // 过滤找到目标的注解
                    if (qualifiedName == canonicalName) {
                        println("==【Kt】===========即将开始生成文件")
                        // 根据类上标记的注解生成字节码文件
                        generateFile(typeElement, roundEnvironment)
                    }
                }
            }
            return false
        }
    
        /**
         * 生成findView的文件,后边用于反射赋值
         *
         * @param typeElement 注解元素
         * @param roundEnvironment 当前环境
         */
        private fun generateFile(typeElement: TypeElement, roundEnvironment: RoundEnvironment?) {
            if (roundEnvironment == null) {
                println("==【Kt】===========roundEnvironment is Null")
                return
            }
            // Step1: 根据指定注解找到被注解标记的类
            val elementsAnnotatedWithClass = roundEnvironment.getElementsAnnotatedWith(typeElement)
    
            // Step2: 遍历每一个被注解标记的类
            elementsAnnotatedWithClass.forEach continuing@{
                if (it is TypeElement) {
                    val simpleName = it.simpleName.toString()
                    println("====【Kt】===== simpleName:$simpleName")
                    // Step3: 构造类里边的方法
                    val findViewFun = generateFindViewFun(it)
                    // Step4: 构造类
                    val findViewTypeSpec = generateFindViewTypeSpec(it, findViewFun)
                    // Step5: 构造文件File
                    val fileSpec = generateFileSpec(it, findViewTypeSpec)
                    // Step6: 写入文件
                    fileSpec.writeFile()
                }
            }
        }
    
        /**
         * 使用kotlinpoet构建方法
         */
        private fun generateFindViewFun(elementsAnnotatedClass: TypeElement): FunSpec {
            // kotlinpoet构造方法
    //        val funSpec = FunSpec.builder("bindView")
    //            .addModifiers(KModifier.PUBLIC, KModifier.FINAL)
    //            .addParameter(ParameterSpec("activity", getClassNameSimpleName(elementsAnnotatedClass)))
    //            .returns(UNIT)
            val kModifiers = setOf(KModifier.PUBLIC, KModifier.FINAL)
            val parameterSpec = ParameterSpec("activity", getClassName(elementsAnnotatedClass))
            val funSpecBuilder = mFunSpecUtil.generateCompanionPublicBuilderFun("bindView", modifiers = kModifiers, parameterSpec = parameterSpec)
    
    
    
    
            // 获取该类中需要赋值的成员
            val elementList = mElementUtil.getAllMembers(elementsAnnotatedClass)
            elementList.forEach continuing@{ classMember ->
                // 从成员上获取指定的注解 (findViewById)
    //            val viewIdAnnotation = classMember.getAnnotation(ViewId::class.java) /*?: return@continuing*/
    //            if (viewIdAnnotation != null) {
    //                funSpecBuilder.addStatement(findViewByIdStatement(classMember, viewIdAnnotation))
    //            }
                addStatementByAnnotationType(classMember, ViewId::class.java, funSpecBuilder)
            }
            return funSpecBuilder.build()
        }
    
        // TODO: 12/24/21 工厂模式  -》 返回语句
        private fun <A : Annotation> addStatementByAnnotationType(classMember: Element, typeClass: Class<A>, funSpecBuilder: FunSpec.Builder) {
            val annotation = classMember.getAnnotation(typeClass)
            if (annotation != null) {
    
                when (typeClass) {
                    ViewId::class.java -> {
                        println("====> ViewId::class.java")
                        funSpecBuilder.addStatement(findViewByIdStatement(classMember, (annotation as ViewId)))
                    }
                    AutoWire::class.java -> {
                        println("====> AutoWire::class.java")
    
                    }
                }
            }
        }
    
        /**
         * findViewById的语句
         */
        private fun findViewByIdStatement(classMember: Element, annotation: ViewId): String {
            return String.format(
                Locale.CHINA,
                "activity.%s = activity.window.decorView.findViewById<%s>(%s)",
                classMember.simpleName,
                classMember.asType().toString(),
                annotation.viewId
            )
        }
    
        /**
         * 使用kotlinpoet构建生成类
         */
        fun generateFindViewTypeSpec(typeElement: TypeElement, funSpec: FunSpec): TypeSpec {
            // 构建属性
    //        val propertiesSpec1 = mPropertiesSpecUtil.generatePropertiesSpec("NAME", STRING, false, """"Oracle version 1.8.12"""", KModifier.PUBLIC)
    //        val propertiesNameSpec = generateCompanionProperties("NAME", STRING, KModifier.PRIVATE)
    //        propertiesSpecSet.add(propertiesSpec1)
    //
    //        val propertiesAgeSpec = generateCompanionProperties("AGE", INT, KModifier.CONST)
    //        propertiesSpecSet.add(propertiesAgeSpec)
    
            // 静态变量
    
            val propertyCompanionSpec1 = mPropertiesSpecUtil.generatePropertySpec(
                "LOVE",
                BOOLEAN,
                mutable = false,
                initialFormat = String.format("%b", true),
                modifier = KModifier.CONST
            )
            propertySpecCompanionSet.add(propertyCompanionSpec1)
            // 构建生成静态代码块  伴生对象只能存在一个
            val companionTypeSpec = generateCompanionFun(funSpec)
    
            println("====》 数量:${typeSpecSet.size}")
            // 类变量
            // 生成可读可写类变量1
            // val propertyClassSpec1 = mPropertiesSpecUtil.generatePropertySpec("name", STRING, mutable = false, """"马小跳"""")
            // 生成可读不可写类变量2
            //  val propertyClassSpec2 = mPropertiesSpecUtil.generatePropertySpec("address", STRING, initialFormat = """""上海市马家庄1号"""")
    
            // kotlinpoet构造类
            val className = getClassName(typeElement).simpleName + "_BindView"
    
    
            return TypeSpec.classBuilder(className)
                .addType(companionTypeSpec)
                // 往类里边添加方法
                .addFunction(funSpec)
                .build()
    //        return mClassSpecUtil.generateClassSpec(className, typeSpecSet = typeSpecSet/*, typeSpec = companionTypeSpec*/)
    //        return TypeSpec.classBuilder(getClassNameSimpleName(typeElement) + "_BindView")
    //            .addProperties(propertiesSpecSet)
    //            .addTypes(typeSpecSet)
    //            .addModifiers(KModifier.PUBLIC, KModifier.FINAL)
    //            .addFunction(funSpecModel)
    //            .addFunctions(funSet)
    //            .build()
        }
    
        /**
         * 新建一个简单的类:Hello World
         */
        private fun helloWorld(): TypeSpec {
            val android = PropertySpec.builder("androidVersion", STRING)
                .addModifiers(KModifier.PRIVATE)
                .initializer("""Android S""")
                .build()
    
            return TypeSpec.classBuilder("HelloWorld")
                .addProperty(android)
                .addProperty("NDK_VERSION", STRING, KModifier.PRIVATE)
                .build()
        }
    
        /**
         * 构建静态代码块
         */
        private fun generateCompanionFun(funSpecModel: FunSpec): TypeSpec {
            return TypeSpec.companionObjectBuilder()
                .addProperties(propertySpecCompanionSet)
                // 往companion object中添加方法
    //            .addFunction(funSpecModel)
                .build()
        }
        /**
         * 构建生成静态属性
         */
        private fun generateCompanionProperties(propertiesName: String, typeName: TypeName, modifiers: KModifier): PropertySpec {
            return PropertySpec.builder(propertiesName, typeName, modifiers)
                .mutable()
                .initializer(""""Oracle version 1.8.12"""")
                .build()
        }
    
        /**
         * 将构建的类model写入,生成实体文件
         */
        private fun generateFileSpec(typeElement: TypeElement, typeSpec: TypeSpec): FileSpec {
            val packageName = getPackageName(typeElement)
            val fileName = getClassName(typeElement).simpleName + "_BindView"
            return FileSpec.builder(packageName, fileName)
                .addType(typeSpec)
                .build()
        }
    
        /**
         * 在IO Thread写入文件
         */
        private fun FileSpec.writeFile() {
            val kaptKotlinGeneratedDir = processingEnv.options["kapt.kotlin.generated"]
            kaptKotlinGeneratedDir?.let {
                val outputFile = File(it).apply {
                    mkdirs()
                }
                println("====> 文件数量  -> 路径:${outputFile.toPath()}    it:$it")
                writeTo(outputFile.toPath())
            }
        }
    
        /**
         * 获取包名
         * @param element typeElement
         * @return 返回包名
         */
        private fun getPackageName(element: Element): String {
            // Qualified 获取全路径名称
            println("包名--> ${mElementUtil.getPackageOf(element).qualifiedName}")
            return mElementUtil.getPackageOf(element).qualifiedName.toString()
        }
    
        /**
         * 获取ClassName的
         * @return ClassName()
         */
        private fun getClassName(element: Element): ClassName {
            val packageName = getPackageName(element)
            val simpleName = element.simpleName.toString()
            return ClassName(packageName, simpleName)
        }
    }
    

    最后经过编译之后就会生成文章开头的目标文件。这片入门文章到此完毕,EventBus、ARouter等框架都是用了APT,实战内容后边会介绍、下一片介绍KSP。

    这里推荐一篇介绍KotlinPoet语法的文章

    相关文章

      网友评论

        本文标题:APT入门

        本文链接:https://www.haomeiwen.com/subject/weaxqrtx.html