抬头仰望星空,是否能发现自己的渺小。

伪斜杠青年

人们总是混淆了欲望和理想

Kotlin – Annotation Processing

其实2020年了,谈这个已经不合时宜了,但是呢,既然学了,还是总结一下。

电脑在导视频,13寸的 MBP 太慢,所以写一写

其实对于一般情况来说,工作中很少或者基本上用不到自己写 Annotation,更别说Annotation Processing,所以不学了吗?不,不是的,这个基础是必须的,因为会在第三方的框架中频繁看到,比如 ButterKnife,不过现在已经是 kotlin 的时代了,kotlin-android-extention可以很好的帮我们绑定元素(更神秘一点,需要 decompile才能看到,本质依旧是替代 findViewById),但通过实现ButterKnife依旧是很好的Annotation Processing学习入口。

不喜欢写废话(内容太多了),一条龙服务。

方案一:注解 + 反射

注解就是随便写啦:

/**
 * 目标对象 FIELD 字段
 */
@Target(AnnotationTarget.FIELD)
/**
 * 运行时依旧存在
 */ 
@Retention(AnnotationRetention.RUNTIME)
annotation class BindView(val value:Int)

说明一下:value 是一个默认字段,是为了方便将 @BindView(value = R.id.tv_hello) 写成 @BindView(R.id.tv_hello)

再来一个类替我们做 findViewById 以及给字段赋值的操作:

class Binding {

    companion object{
        fun bind(act: Activity) {
            act.javaClass.declaredFields.forEach { field ->
                val bindView = field.getAnnotation(BindView::class.java)
                bindView?.also {
                    field.isAccessible = true //可能不是 open 放开权限
                    field.set(act, act.findViewById(bindView.value))
                }
            }
        }
    }
}

然后在 activity 里使用:

class MainActivity : AppCompatActivity() {

    @BindView(R.id.tv_hello)
    lateinit var hello:TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Binding.bind(this)
        hello.text="哈哈"
    }
}

原理很简单,就是遍历activity 里的每个字段,然后使用反射的手法进行赋值,几行代码,有什么问题呢?问题就是效率太慢。反射到底多重不知道,毕竟有人手机快,有人手机卡,但一定比普通代码要重。

方案二:注解 + 反射 + Annotation Processing

对方案一改进一下顺便再加上自动化,也就是把反射只用于创建binding 类,binding 类的内容嘛,像这样:

那反射的部分就可以做了:

class Binding {
    companion object{
        fun bind(act:Activity){
            //new MainActivity$Binding
            val bindingClass = Class.forName("${act.javaClass.canonicalName}\$Binding")
            val activityClass = Class.forName(act.javaClass.canonicalName)
            val constructor = bindingClass.getDeclaredConstructor(activityClass)
            constructor.newInstance(act)
        }
    }
}

关于canonicalNameJava Doc 简单说就是我们自己定义的类名,匿名内部类则返回 null,相对于其他获取 name 的方法,这个方法返回的内容是确定的。

如果上面的 bind 方法,传入的是MainActivity则会创建一个MainActivity$Binding的实例,从而初始化各个字段。

问题是MainActivity$Binding要怎么拼呢?IO 手写?没问题,够强都 ok。但我们毕竟不是第一个需要这种需求的人,拿来主义:javapoet

单独建一个 annotation_processing 的 Module ,普通的 Java lib 即可。

依赖:

implementation 'com.squareup:javapoet:1.13.0'

然后创建类继承AbstractProcessor:

class BindingProcessor : AbstractProcessor()

关于AbstractProcessor有三个方法需要覆写:

  • init 方法,主要是用于获取生成文件的 filer 类
override fun init(environment: ProcessingEnvironment) 
  • process 真正处理文件的地方,用于书写文件处理流程 ,roundEnv 代表每一个类文件
override fun process(
annotations: MutableSet<out TypeElement>?,
roundEnv: RoundEnvironment
): Boolean
  • getSupportedAnnotationTypes 用于指定支持哪些注解,用于过滤
override fun getSupportedAnnotationTypes(): MutableSet<String>

最后创建一个用于识别该 processor 的配置文件,依次创建文件层级:

annotation_processing/src/main/resources/META-INF/services/javax.annotation.processing.Processor

内容只需要敲上 com 就会出来我们上面写的 BindingProcessor 类。写到这里,是时候提一提 kapt 了,在以前用 java 时使用的是 annotationprocessor 进行编译,在 kotlin 中已经改为 kapt。需要让 kapt 知晓。

在主工程的依赖中进行处理:

kapt project(":annotation_processing")

处理好相关依赖,大致步骤与逻辑就说完了。

BindingProcessor具体实现?

第一步,在 init 中拿到 filer:

class BindingProcessor : AbstractProcessor() {

    lateinit var filer: Filer

    override fun init(environment: ProcessingEnvironment) {
        super.init(environment)
        filer = environment.filer
    }
//....
}

第二步,指定清楚需要处理哪些注解(这里依旧使用 canonicalName 原因就是因为canonicalName是不变的):

override fun getSupportedAnnotationTypes(): MutableSet<String> {
//指定需要支持哪些 annotation
return mutableSetOf(BindView::class.java.canonicalName)
}

第三步,生成指定逻辑的类文件:

override fun process(
annotations: MutableSet<out TypeElement>?,
roundEnv: RoundEnvironment
): Boolean {
println("annotation processor running!!")

for (element in roundEnv.rootElements) {
val packageStr = element.enclosingElement.toString()
val classStr = element.simpleName.toString()
val className = ClassName.get(packageStr, "$classStr\$Binding")
val constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get(packageStr, classStr), "activity")

var hasBinding = false
for (enclosedElement in element.enclosedElements) {
val bindView = enclosedElement.getAnnotation(BindView::class.java)
if (bindView != null) {
hasBinding = true
constructorBuilder.addStatement(
"activity.\$N = activity.findViewById(\$L)",
enclosedElement.simpleName, bindView.value
)
}
}
val builtClass = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addMethod(constructorBuilder.build())
.build()
if (hasBinding) {
JavaFile.builder(packageStr, builtClass)
.build().writeTo(filer)
}
}
return false
}

其实就是将一系列的类结构使用代码进行了生成。至于相关内容,可根据变量名判断,其次是寻找官方的 api 文档。值得一提的是这种写法为粗糙写法:

for (element in roundEnv.rootElements) 

真正细腻化的操作,可按 annotation 进行搜寻,api 为:

roundEnv.getElementsAnnotatedWith(BindView::class.java)

这样返回的均为 BindView 注解所修饰的字段。这里网上有很多将 Elements 用一个类举例进行拆解的,我不确定其真实性,所以也不打算复制,官方文档值得一看:

https://docs.oracle.com/javase/8/docs/api/javax/lang/model/element/TypeElement.html

我未在文档中看到准确的获取各元素的方法,大概这也是为什么三方框架都需要使用特定标识或者annotation的原因吧。

ButterKnife 算依赖注入吗?

首先什么是依赖注入呢?看网上各种 spring 各种控制反转,就不能来个通俗易懂的解释吗?答案是:能,不过得看 依赖注入 – wiki 百科

原文:

在软件工程中,依赖注入(dependency injection)的意思为,给予调用方它所需要的事物。 

显而易见,ButterKnife不是,因为字段最终是靠传入的 activity 本身去 findViewById 进行初始化的。或者说是,Activity⾃己决定依赖的的获取,只是把执行过程交给了 ButterKnife,过程中不存在把依赖的决定权交给外部。

以上,仅做思路总结,源码就不上了。Hencoder Plus 的内容,可在凯哥 github 上找到(当然,就不是 kotlin 版了,但本质无差)。


本站由以下主机服务商提供服务支持:

0条评论

发表评论