其实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) } } }
关于canonicalName:Java 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条评论