Konlin注解处理器——简易版ButterKnife实现
1. ButterKnife简介
ButterKnife是一个专注于Android系统的View 注入框架,它通过在编译期生成class文件,为开发者自动完成findViewById 方法的调用,对注解的View 进行实例绑定。 ButterKnife最基本的使用方法分为4步: 1.在build.gradle 中添加依赖
apply plugin: 'kotlin-kapt'
implementation 'com.jakewharton:butterknife:10.1.0'
kapt 'com.jakewharton:butterknife-compiler:10.1.0'
2.对Activity中的View 添加@BindView 注解。
@BindView(R.id.tv)
lateinit var tv: TextView
3.在Activity 的onCreate 方法中调用setContentView 之后,调用ButterKnife.bind(this) 对所有的添加了注解的View 进行实例绑定。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycler_view)
ButterKnife.bind(this)
}
4.在Activity 中无需调用findViewById 方法对View 进行赋值,即可直接使用。
tv.text = "Hello World!!!"
本文要实现的功能就是这样一个最基础的简易版ButterKnife。
2. 正文前的说明
- Kotlin提供的
kotlin-android-extensions 插件已经提供了很方便的View 自动绑定功能,所以使用Kotlin时是没必要使用ButterKnife的(个人观点)。 - 本文的目的是介绍和记录在kotlin中APT(Annotation Processing Tool,注解处理器)的使用方法,以及如何使用KotlinPoet自动生成kotlin代码,以及在这期间自己踩得一些坑,如果只关心代码实现,可以直接跳转到最后一章,或者查看源代码。
- 以下内容纯属个人见解结合网上资料完成。存在错误,实属正常,如有不足,欢迎指正。
3. 自动绑定View的原理
实现View 的自动绑定需要3个类之间进行合作。
首先,Activity中提供带绑定的View ,同时调用ButterKnife.bind(this) 完成绑定。代码见ButterKnife简介中的第三代码段
其次,ButterKnife类中提供静态方法bind(activity:Activity) 在该方法中通过反射实例化一个Binding类,同时传入activity 作为实例化的参数,这个Binding类与具体传入的Activity 类相关(即,一个Activity 对应一个Binding 类,Binding类的命名规则:ButterKnife_**_Binding ,** 为Activity 的类名)。具体代码如下:
class ButterKnife {
companion object {
fun bind(activity: Activity) {
val clazzName = "${activity.javaClass.`package`.name}.ButterKnife_${activity.javaClass.simpleName}_Binding"
val clazz = Class.forName(clazzName)
val constructor = clazz.getConstructor(activity.javaClass)
constructor.newInstance(activity)
}
}
}
最后,在Binding 类的构造方法中利用activity实例调用findViewById() 方法进行View 的绑定。具体代码如下:
public class ButterKnife_MainActivity_Binding() {
public constructor(activity: MainActivity) : this() {
activity.tv=activity.findViewById(2131230814)
}
}
说明:
- 为什么ButterKnife的
bind() 方法要用反射?因为每个Activity 都有自己的Binding 类,两者之间只有类名相关,反射调用Binding 类的构造方法,在构造方法对View 进行赋值,可以为所有的Activity 提供统一的绑定View 的方式。 - 反射不消耗性能么?事实上只是通过反射调用构造方法,并没有反射遍历所有属性并分析注解这种耗时操作,和虚拟机构造一个类的实例差不多。
- 每个Activity对应一个
Binding 类,命名还有要求,写代码不是变复杂了?事实上,Binding 类是APT工具在编译期使用KotlinPoet自动生成的。ButterKnife类只有一个,并且写在一个单独的Module里,所以在使用时只需要在Activity中对应的View 上打注解,然后调用ButterKnife.bind(this) 即可。 @BindView 注解的作用是什么?辅助APT生成对用的Binding类。- 其他注意事项:
View 不能是private ,且要声明为lateinit var ,不然在Binding 类中无法赋值。
4. APT的使用
APT,即注解处理器。在Android中,使用gradle将源文件编译打包成Android的APK文件,事实上是执行了gradle插件中的一个个task,这些task负责完成不同的任务。下图(偷来的,点击查看原文)展示了Android的编译打包流程(缺少签名的过程),APT的工作与图中箭头所指的aapt类似(APT和aapt是两个东西),即APT会在task调用javac对源文件进行编译前被调用,根据APT的代码生成Generated Source Files,生成的代码会和其他的Source Files一起被javac编译成class文件,放进最终的apk中。 编写APT需要两个要素:注解和注解处理器,使用方法如下。
-
创建注解处理器对应的库:New->Module->Java or Kotlin Libray,填写库名和类名->Finish
说明:Module一定是Java or Kotlin Libiary,否则注解处理器无法生效。事实上,注解处理器确实不算Android Library,因为它是工作在编译期间的。库名和类名可以随意,但是后面会被用到。请忽略图中的报错,因为不想删库重新创建一遍。
-
在butterknife-annotation-lib/src/main目录下创建文件夹 resources/META-INF/services,并在services文件夹下创建文件javax.annotation.processing.Processor。这一步的每一个文件夹和文件的命名都是固定的,不能修改。最后在javax.annotation.processing.Processor文件中写注解处理器对应类的全限定名。本文中是:com.cam.butterknife_compile_lib.ButterKnifeAnnotationProcessor
说明:此步配置是为了让gradle在编译前将注解处理器(ButterKnifeAnnotationProcessor )识别出来并执行其中的代码。
-
创建注解类。步骤2类似,创建一个Java or Kotlin Libiary的模块,模块名随意,本文为butterknife-annotation-lib 。在模块中创建BindView 注解。 BindView注解的代码为: @Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FIELD)
annotation class BindView(val value:Int)
说明:
@Retention(AnnotationRetention.SOURCE) 说明注解只会存活在源码中,在编译阶段使用。@Target(AnnotationTarget.FIELD) 说明注解使用在属性上的。value 用来记录View的id 。- 将
@BindView 注解放在Java or Kotlin Libiary 中,是因为注解处理器后面要读取这个注解,Activity 也会使用这个注解,如果是Android的Module,注解处理器的类读取注解时会有问题。也可以将@BindView 注解放在和注解处理器同一模块,但是那样Activity所在的模块添加依赖时就会很丑陋。
-
添加依赖。 在app 模块的build.gradle 添加对butterknife-compile-lib 模块的依赖(注解处理器模块)、butterknife-annotation-lib 模块的依赖(提供注解)、以及声明kapt 插件。代码如下: plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
dependencies {
implementation project(path: ':butterknife-annotation-lib')
kapt project(path: ':butterknife-compile-lib')
}
说明:注解处理器的依赖必须用kapt ,不能用annotationProcessor ,否则无法识别打在Kotlin代码上的注解,只能识别打在Java代码上的注解。使用kapt插件,既能识别打在Java上的注解,也能识别打在Kotlin上的注解。
butterknife-compile-lib 模块添加对butterknife-annotation-lib 模块的依赖,同时把Java的版本改成Java8(因为后面使用KotlinPoet对Java的版本有要求,不是Java8会报错)。为了方便,添加上KotlinPoet依赖(反正后面迟早要添加),代码如下: java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation project(path: ':butterknife-annotation-lib')
implementation "com.squareup:kotlinpoet:1.10.2"
}
最后把butterknife-annotation-lib 模块中的java版本也改成Java8,不改会怎样呢?没试过!懒得试!修改方法相同,不在赘诉。 -
修改注解处理器代码。编写步骤2中创建的ButterKnifeAnnotationProcessor 的代码,使其继承AbstractProcessor 并重写其中的方法,代码如下。 class ButterKnifeAnnotationProcessor : AbstractProcessor() {
lateinit var filer: Filer
override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv)
filer = processingEnv.filer
}
override fun getSupportedAnnotationTypes(): MutableSet<String> {
println("getSupportedAnnotationTypes is running")
val x = mutableSetOf(BindView::class.java.canonicalName)
return x
}
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latestSupported()
}
override fun process(p0: MutableSet<out TypeElement>?, p1: RoundEnvironment?): Boolean {
if (p0.isNullOrEmpty() || p1 == null) {
return false
}
println("process is running........")
return true
}
}
说明:
init(processingEnv: ProcessingEnvironment) 方法是注解处理器在初始化阶段调用的,代码中为filer 成员变量赋值,是因为在生成代码时会用到Filer对象,获得Filer对象是常用操作(虽然此处示例并没有用到)。getSupportedAnnotationTypes() 方法返回一个集合,集合中包含这个注解处理器包含的注解类对象。如果编译期间代码中没有包含这些注解,则process() 方法不会被调用。getSupportedSourceVersion() ,固定写法。process() 用来处理注解信息的,是注解处理器的核心方法。该方法会被多次调用,因为注解处理器生成的代码 也可能 包含需要处理的注解,如果包含的话,process 会被再次调用,此时会被用来处理生成代码上包含的注解。这也是RoundEnvironment 命名由来,Round即回合。process() 返回true,表明注解被当前注解处理器处理,返回false,表示注解处理器没有处理这个注解,这个注解可能会被其他注解处理器处理。p1 == null 表示本回合没有需要处理的注解,返回即可。
-
在MainActivity的布局文件中,将HelloWorld的TextView设置id为tv,在MainActivity中声明控件并打上@BingView 注解,运行项目,运行结果如图所示。
说明:
- 图中1处红框说明,我们的注解处理器被当作gradle的task在执行,而且执行在其他的task之前。
- 图中2处红框说明,我们的注解处理器按预期执行并输出。
- MainActivity中的tv不能使用,因为还没有对其赋值
- 如果代码中没有使用
@BindView 注解,则process() 方法不会被执行 - 如果使用了annotationProcessor而没有使用kapt导入注解处理器的依赖,则打在Kotlin代码上的
@BindView 注解不会触发process() 方法,打在Java代码上的@BindView 可以触发process() 方法。(PS:卡了我一天的bug)
自此,注解处理器相关的内容基本结束,下面将讲诉如何进行代码生成。
5. KotlinPoet的使用
KotlinPoet是类似于JavaPoet的库,主要用于自动生成Kotlin代码,详细使用方法见KotlinPoet官网 。
本文针对KotlinPoet要讲的核心内容是:
KotlinPoet针对Kotlin提供了File级、Class级、Function级、Property级的Spec描述一个kotlin源文件,然后通过Builder模式进行组装。例如:一个描述类的Class级Spec,通过组装一个描述方法的Function级Spec向类中添加方法(包括普通成员方法和构造方法),通过组装描述属性的Property级Spec向类中添加属性,最后将这个Class级Spec组装进描述.kt文件的File级Spec中形成最终的Kotlin源文件。File级Spec提供了write 方法,可以将描述的 .kt文件写进输出流或者APT中的Filer中。
PS:本章以下代码是KotlinPoet的一个示例代码,示例代码与本章主要内容无关,纯粹是用来记录KotlinPoet的使用方法的,跳过不影响全文阅读。KotlinPoet需要添加的依赖见第4章
本章生成的源文件(文件名:KotlinPoem.kt )中包含一个接口(PoemPrinter ),3个类(PoemMaker 、Poem 、KotlinPoem )和一个main方法,KotlinPoem 实现了PoemPrinter 接口,覆写了printPoem() 方法,打印PoemMaker 写的Poem (PoemMaker 和Poem 是KotlinPoem 的成员变量),返回打印的字符数。示例包括了如何定义类、实现接口、定义属性、设置修饰符、编写构造方法、属性初始化、获得类名、生成源文件等。
示例代码:
package com.cam.ktl
import com.squareup.kotlinpoet.*
import java.io.File
private const val packageName = "com.cam.ktl"
private const val fileName = "KotlinPoem"
private val stringClassName = ClassName("kotlin", "String")
private val intClassName = ClassName("kotlin", "Int")
fun getPoemMakerClass(): TypeSpec {
val poemMakerConstructor = FunSpec.constructorBuilder()
.addParameter("name", stringClassName)
.addParameter("age", intClassName)
.build()
return TypeSpec.classBuilder("PoemMaker")
.primaryConstructor(poemMakerConstructor)
.addProperty(PropertySpec.builder("name", stringClassName).initializer("name").build())
.addProperty(PropertySpec.builder("age", intClassName).initializer("age").build())
.build()
}
fun getPoemClass(): TypeSpec {
val poemMakerConstructor = FunSpec.constructorBuilder()
.addParameter("title", stringClassName)
.addParameter("content", stringClassName)
.build()
return TypeSpec.classBuilder("Poem")
.primaryConstructor(poemMakerConstructor)
.addProperty(PropertySpec.builder("title", stringClassName).initializer("title").build())
.addProperty(PropertySpec.builder("content", stringClassName).initializer("content").build())
.build()
}
fun getPoemPrinterInterface(): TypeSpec {
val printFun = FunSpec.builder("printPoem")
.returns(intClassName)
.addModifiers(KModifier.ABSTRACT)
.build()
return TypeSpec.interfaceBuilder("PoemPrinter")
.addFunction(printFun)
.build()
}
fun getKotlinPoemClass(): TypeSpec {
val primaryConstructor = FunSpec.constructorBuilder()
.addModifiers(KModifier.PRIVATE)
.build()
val poemMakerClazzName = ClassName(packageName, "PoemMaker")
val makerParameterName = ParameterSpec.builder("maker", poemMakerClazzName)
.defaultValue("PoemMaker(%S, 0)", "")
.build()
val poemClazzName = ClassName(packageName, "Poem")
val poemParameterName = ParameterSpec.builder("poem", poemClazzName)
.defaultValue("Poem(%S, %S)", "", "")
.build()
val secondConstructor = FunSpec.constructorBuilder()
.addModifiers(KModifier.PUBLIC)
.addParameter(makerParameterName)
.addParameter(poemParameterName)
.callThisConstructor()
.addCode(
"""
this.maker = maker
this.poem = poem
""".trimIndent()
)
.build()
val printFunc = FunSpec.builder("printPoem")
.returns(intClassName)
.addStatement(
"""
var wordNum = 0
val title = poem.title
wordNum += title.length
println(title)
val authorInfo = maker.name +" "+ maker.age
wordNum += authorInfo.length
println(authorInfo)
val content = poem.content
wordNum += content.length
println(content)
return wordNum
""".trimIndent()
)
.addModifiers(KModifier.OVERRIDE)
.build()
val poemPrinterInterface = ClassName(packageName, "PoemPrinter")
return TypeSpec.classBuilder("KotlinPoem")
.primaryConstructor(primaryConstructor)
.addSuperinterface(poemPrinterInterface)
.addFunction(secondConstructor)
.addProperty(
PropertySpec.builder("maker", poemMakerClazzName).addModifiers(KModifier.LATEINIT)
.mutable(true)
.build()
)
.addProperty(
PropertySpec.builder("poem", poemClazzName).addModifiers(KModifier.LATEINIT)
.mutable(true)
.build()
)
.addFunction(printFunc)
.build()
}
fun getMainFun(): FunSpec {
return FunSpec.builder("main")
.addStatement(
"""
val poemStr = ""${'"'}nothing is all you need""${'"'}
val poem = KotlinPoem(
maker = PoemMaker("CAM", 25),
poem = Poem(
title = "nothing", content = poemStr
)
)
val wordNum = poem.printPoem()
println("====We have print ${"\$"}wordNum Characters ====")
""".trimIndent()
)
.build()
}
private fun write(fileSpec: FileSpec) {
val f = File("./temp")
fileSpec.writeTo(f)
}
fun main() {
val fileSpec = FileSpec.builder(packageName, fileName)
.addType(getPoemMakerClass())
.addType(getPoemClass())
.addType(getPoemPrinterInterface())
.addType(getKotlinPoemClass())
.addFunction(getMainFun())
.build()
write(fileSpec)
}
运行后生成代码:
package com.cam.ktl
import kotlin.Int
import kotlin.String
import kotlin.Unit
public class PoemMaker(
public val name: String,
public val age: Int
)
public class Poem(
public val title: String,
public val content: String
)
public interface PoemPrinter {
public fun printPoem(): Int
}
public class KotlinPoem private constructor() : PoemPrinter {
public lateinit var maker: PoemMaker
public lateinit var poem: Poem
public constructor(maker: PoemMaker = PoemMaker("", 0), poem: Poem = Poem("", "")) : this() {
this.maker = maker
this.poem = poem
}
public override fun printPoem(): Int {
var wordNum = 0
val title = poem.title
wordNum += title.length
println(title)
val authorInfo = maker.name +" "+ maker.age
wordNum += authorInfo.length
println(authorInfo)
val content = poem.content
wordNum += content.length
println(content)
return wordNum
}
}
public fun main(): Unit {
val poemStr = """nothing is all you need"""
val poem = KotlinPoem(
maker = PoemMaker("CAM", 25),
poem = Poem(
title = "nothing", content = poemStr
)
)
val wordNum = poem.printPoem()
println("====We have print $wordNum Characters ====")
}
生成代码运行结果如下:
6. 实现ButterKnife的最终流程
终于见到了最终Boss,剩下的内容不多了,加油!!! 现在需要做5件事即可完成项目,编写ButterKnife类及bind()静态方法完成绑定、编写注解处理器生成Binding类的代码、重新设置依赖、使用ButterKnife、运行。
-
编写ButterKnife类。通过New->Module->Android Library,创建一个Android的Library模块,命名随意,本文为butterknife-ib 。新建类ButterKnife,代码如下: class ButterKnife {
companion object {
fun bind(activity: Activity) {
val clazzName = "${activity.javaClass.`package`.name}.ButterKnife_${activity.javaClass.simpleName}_Binding"
val clazz = Class.forName(clazzName)
val constructor = clazz.getConstructor(activity.javaClass)
constructor.newInstance(activity)
}
}
}
-
重新编写注解处理器的process() 方法,代码如下: override fun process(p0: MutableSet<out TypeElement>?, p1: RoundEnvironment?): Boolean {
if (p0.isNullOrEmpty() || p1 == null) {
return false
}
println("process is running........")
for (element in p1.rootElements) {
val packageName = element.enclosingElement.toString()
val className = element.simpleName
var needWrite = false
val clazzActivity = ClassName.bestGuess("${packageName}.${className}")
val constructorFun = FunSpec.constructorBuilder()
.addParameter("activity", clazzActivity)
.callThisConstructor()
for (innerElement in element.enclosedElements) {
val annotationElement = innerElement.getAnnotation(BindView::class.java)
if (annotationElement != null) {
needWrite = true
constructorFun.addStatement("activity.${innerElement} =activity.findViewById(${annotationElement.value})")
println("$packageName.$className.$innerElement")
}
}
if (needWrite) {
val bindClassName = "ButterKnife_${className}_Binding"
val classType = TypeSpec.classBuilder(bindClassName)
.primaryConstructor(FunSpec.constructorBuilder().build())
.addFunction(constructorFun.build())
.build()
val file = FileSpec.builder(packageName, bindClassName)
.addType(classType)
.build()
file.writeTo(filer)
}
}
return true
}
说明:
- 第6行,
p1.rootElements 会获得源文件中所有的类信息(不管是否有待处理的注解), - 第7行,
element.enclosingElement 会获得类所在的包的信息(enclosingElement 会获得外层元素,类的外层元素即为包) - 第14行,
element.enclosedElements 会获得类中的所有元素 - 第15行,尝试从元素中获得待处理注解
- 第16行,如果成功获得待处理注解,则向构造方法中插入findVIewById()的赋值语句,needWrite置为true,表明需要生成文件。
- 第22-32行,组装FileSpec,并源文件写入filer,文件会出现在生成的文件夹,最终参与编译。
-
添加依赖,向app模块中添加butterknife_lib 的依赖,同时,为了让app 模块的build.gradle 更少,可以将app 模块下的build.gradle 中对butterknife-annotation-lib 模块的依赖去掉,在butterknife_lib 模块中以api的方法添加对butterknife-annotation-lib 模块的依赖(PS:implementationh和api的区别及应用场景请自行百度) 具体代码:
dependencies {
kapt project(path: ':butterknife-compile-lib')
implementation project(path: ':butterknife-lib')
}
dependencies {
api project(path: ':butterknife-annotation-lib')
}
-
在app模块下的MainActivity 中的onCreate() 方法添加ButterKnife.bind(this) ,同时可以使用tv:TextView 。代码如下: class MainActivity : AppCompatActivity() {
@BindView(R.id.tv)
lateinit var tv: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ButterKnife.bind(this)
tv.text = "Hello, this is CAM"
}
}
-
运行!! 在build文件夹下的特定位置生成了我们所需要的类,程序运行结果正常!
7. 参考链接
https://www.jianshu.com/p/019c735050e0 https://square.github.io/kotlinpoet/
|