0718 学习APT,入门

学习APT,今天入门学习一下,APT是Java提供的一个特性。

美滋滋,已经半抄半写的搞了一个最简单的APT,就是类似butterknife去做一个findViewById的功能,主要是用了kapt autoService kotlinpoet ,不得不说,squareup太强了,啥东西都是他们家出的,javapoet是他们家的,retrofit啥的也都是,太强了。

implementation 'com.google.auto.service:auto-service:1.0-rc6'
    kapt 'com.google.auto.service:auto-service:1.0-rc6'
    implementation project (':GrowthAnnotation')
    implementation 'com.squareup:kotlinpoet:0.7.0'

三个新增的library

这里按照步骤讲一下整体的模板是怎么样的,首先建立一个Java Library作为对应的注解声明的地方和全局变量声明的地方,我这里主要是声明类和Filed

package com.xpj.growthannotation

// 定义的注解,编译时作用在类上
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class GrowthClass

// 作用域为类中的变量
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class GrowthFindView(val value: Int = -1)

// 生成类后缀
const val GROWTH_BIND_VIEW = "_growthBindView"
// 方法
const val GROWTH_BIND_VIEW_METHOD = "growthBindView"

然后是对应的GrowthApi这个是一个AndroidLibrary,这个主要是提供和Android的交互,例如我们这边是反射生成对应类然后调用我们生成类的方法。

package com.xpj.growthapi

import com.xpj.growthannotation.GROWTH_BIND_VIEW
import com.xpj.growthannotation.GROWTH_BIND_VIEW_METHOD

class GrowthKapt {
    companion object {
        fun bindView(target: Any) {
            val classs = target.javaClass
            val claName = classs.name + GROWTH_BIND_VIEW
            // 反射生成对应的clazz
            val clazz = Class.forName(claName)

            // 这里找到自己自定义的方法,使用反射调用,这里要和定义的方法对应上,而不是和注解名字对应
            val bindMethod = clazz.getMethod(GROWTH_BIND_VIEW_METHOD, target::class.java)
            // 生成对应的对象实例
            val ob = clazz.newInstance()
            // 反射invoke身上的 GROWTH_BIND_VIEW_METHOD 方法
            bindMethod.invoke(ob, target)
        }
    }
}

我们看看生成的类,

package com.xpj.mygrowthpath

import kotlin.jvm.JvmStatic

// kotlin搭配kotlinpoet yyds,这里生成的类就是生成这种很简洁但不简单的方法,生成的是静态方法,这里用了cb 伴生对象,你把对应的Activity传递进去他帮你调用findViewById并且复制
class SecondActivity_growthBindView {
    companion object {
        @JvmStatic
        fun growthBindView(activity: SecondActivity) {
            activity.text1 = activity.findViewById(2131231051)
            activity.img = activity.findViewById(2131231048)
        }
    }
}

最后就是最重要的模块,GrowthAnnCom

package com.xpj.growthanncom

import com.google.auto.service.AutoService
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asClassName
import com.xpj.growthannotation.GROWTH_BIND_VIEW
import com.xpj.growthannotation.GROWTH_BIND_VIEW_METHOD
import com.xpj.growthannotation.GrowthClass
import com.xpj.growthannotation.GrowthFindView
import java.io.File
import javax.annotation.processing.*
import javax.lang.model.SourceVersion
import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements

@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class GrowthAnnCompiler : AbstractProcessor() {
    private lateinit var mLogger: GrowthLogger
    private var elementUtils: Elements? = null

    // 这里返回对应的支持类型,是一个set
    override fun getSupportedAnnotationTypes(): Set<String> {
        return setOf(GrowthClass::class.java.canonicalName)
    }

    override fun init(processingEnv: ProcessingEnvironment) {
        super.init(processingEnv)
        mLogger = GrowthLogger(processingEnv.messager)
        // 首先是初始化的时候存下来这个utils
        elementUtils = processingEnv.elementUtils

        mLogger.info(" ${this::class.java.simpleName} init")
    }


    override fun process(
        annotations: MutableSet<out TypeElement>,
        roundEnv: RoundEnvironment
    ): Boolean {
        mLogger.info("processor start")

        // 先找到目标声明的类
        val elements = roundEnv.getElementsAnnotatedWith(GrowthClass::class.java)

        // 遍历对应的type
        elements.forEach {
            val typeElement = it as TypeElement
            // 拿到所有的members
            val members = elementUtils!!.getAllMembers(typeElement)

            // 这里是生成的方法,哪里名字错了,因此找不到 FunSpec 属于kotlinpoet,使用 GROWTH_BIND_VIEW_METHOD 这个名字去构造方法,参数是activity 加上JvmStatic的注解
            val bindFunBuilder = FunSpec.builder(GROWTH_BIND_VIEW_METHOD).addParameter(
                "activity",
                typeElement.asClassName()
            ).addAnnotation(JvmStatic::class.java)


            // 对成员进行遍历
            members.forEach { ele ->
                val find: GrowthFindView? = ele.getAnnotation(GrowthFindView::class.java)
                if (find != null) {
                    mLogger.info("find annotation " + ele.simpleName)
                    // 如果找到了,就对 bindFunBuilder 添加执行语句
                    bindFunBuilder.addStatement(
                        "activity.${ele.simpleName} " +
                                "= activity.findViewById(${find.value})"
                    )
                }
            }

            // 这里方法的语句准备好了
            val bindFun = bindFunBuilder.build()


            // 生成对应的类 it.simpleName.toString() + "_bindView" 这个是文件名,这里忘了改了。
            val file = FileSpec.builder(
                getPackageName(typeElement),
                it.simpleName.toString() + "_bindView"
            ).addType(
                // 这里是类名,然后addType是add的一个包含method的type,最后build并且写入文件
                TypeSpec.classBuilder(it.simpleName.toString() + GROWTH_BIND_VIEW).addType(
                    TypeSpec.companionObjectBuilder()
                        .addFunction(bindFun).build()
                ).build()
            ).build()
            file.writeFile()
        }

        mLogger.info("end")

        return true
    }

    private fun getPackageName(type: TypeElement): String {
        return elementUtils!!.getPackageOf(type).qualifiedName.toString()
    }


    // 这里设定生成类的目标地址
    private fun FileSpec.writeFile() {
        val kaptKotlinGeneratedDir = processingEnv.options["kapt.kotlin.generated"]
        val outputFile = File(kaptKotlinGeneratedDir!!).apply {
            mkdirs()
        }
        writeTo(outputFile.toPath())
    }
}

App工程中的使用方式

/**
build.gradle引用
    // 以下为学习kapt所需
    // 这里声明注解
    implementation project(':GrowthAnnotation')
    // 种类声明kapt对应的processor
    kapt project(':GrowthAnnCom')
    // 这里声明调用时期的反射去调用。
    implementation project(':GrowthApi')
*/


package com.xpj.mygrowthpath

import android.annotation.SuppressLint
import android.os.Bundle
import android.os.SystemClock
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.xpj.growthannotation.GrowthClass
import com.xpj.growthannotation.GrowthFindView
import com.xpj.growthapi.GrowthKapt
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

/**
 * author : xpj
 * date : 6/27/21 1:30 PM
 * description :
 */
@GrowthClass
@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {
    @Inject
    lateinit var fucktt: GrowthTest2

    @SuppressLint("NonConstantResourceId")
    @GrowthFindView(R.id.test_second)
    lateinit var text1: TextView

    @SuppressLint("NonConstantResourceId")
    @GrowthFindView(R.id.test_img)
    lateinit var img: ImageView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
        GrowthKapt.bindView(this)
//        val text1 = findViewById<TextView>(R.id.test_second)
        text1.setOnClickListener {
            onBackPressed()
        }
        img.setOnClickListener {
            Toast.makeText(this@SecondActivity,"我是注解生成的imageview",Toast.LENGTH_SHORT).show()
        }
        text1.text = "我是第二个页面,点我返回,我是注解生成"
        fucktt.testForV2()
        test()
    }

    private fun test() {
        SystemClock.sleep(100)
    }
}

关于调试 调试方法还可以使用昨天那个remote方式,今天因为clean了一下,发现我之前写的transform这种自定义插件也可以调试,idea 永远滴神呐。通过调试的时候就可以知道一些流程了,这个是真不错,好家伙本来面向工资编程,没想到学到了一些真本事,调试也是好用的不得了,可以断点调试,有了调试功能对于深度学习一个技能简直太方便了吧。

对于apt还有进阶玩法,就是类似hilt在使用asm将咱们生成的类插进去,而不是还需要再手动调用啥的,例如这个需要手动去设置对应的target过来,有了asm咱们就可以先扫描到对应的特定位置然后咱们再插入目标代码,无论是这种静态的还是构造父类,都是可以解决的,asm搭配apt,yyds。

注册Processor Processor需要注册一下才能被注解处理器处理,在src/main/resources/META-INF/services下创建一个javax.annotation.processing.Processor文件,如果没有当前目录就新建一个。然后在对应文件写出注解处理器的全路径名,这个明显有些繁琐,所以我们这边就是使用了google家的autoServices去做注解的插入。

写的不错的文章,比较详细

项目诞生借鉴地