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'

这里按照步骤讲一下整体的模板是怎么样的,首先建立一个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去做注解的插入。