Kotlin编译时注解,简单实现ButterKnife
Posted doubleyoujs
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin编译时注解,简单实现ButterKnife相关的知识,希望对你有一定的参考价值。
ButterKnife在之前的android开发中还是比较热门的工具,帮助Android开发者减少代码编写,而且看起来更加的舒适,于是简单实现一下ButterKnife,相信把下面的代码都搞懂,看ButterKnife的难度就小很多。
今天实现的是编译时注解,其实运行时注解也一样能实现ButterKnife的效果,但是相对于编译时注解,运行时注解会更耗性能一些,主要是由于运行时注解大量使用反射。
一、创建java library(lib_annotations)
我这里创建3个annotation放在3个文件中
//绑定layout
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
annotation class BindLayout(val value: Int = -1)
//绑定view
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class BindView (val value:Int = -1)
//点击注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.BINARY)
annotation class OnClick (vararg val values:Int)
Kotlin对编译时注解时Retention 并没有太多的要求,一般我们使用AnnotationRetention.BINARY或者SOURCE,但是我发现ButterKnife用的是Runtime,测试也可以。
但具体为什么用,不是特别明白,自己认为是AnnotationRetention.RUNTIME基本包含了BINARY或者SOURCE的功能,还支持反射。
二、创建java library(lib_processor)
@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class BindProcessor : AbstractProcessor() {
companion object {
private const val PICK_END = "_BindTest"
}
private lateinit var mLogger: Logger
//存储类文件数据
private val mInjectMaps = hashMapOf<String, InjectInfo>()
//必须实现方法
override fun process(
annotations: MutableSet<out TypeElement>?,
roundEnv: RoundEnvironment
): Boolean {
//里面就要生成我们需要的文件
roundEnv.getElementsAnnotatedWith(BindLayout::class.java).forEach {
bindLayout(it)
}
roundEnv.getElementsAnnotatedWith(BindView::class.java).forEach {
bindView(it)
}
roundEnv.getElementsAnnotatedWith(OnClick::class.java).forEach {
bindClickListener(it)
}
mInjectMaps.forEach { (name, info) ->
//这里生成文件
val file= FileSpec.builder(info.packageName, info.className.simpleName + PICK_END)
.addType(
TypeSpec.classBuilder(info.className.simpleName + PICK_END)
.primaryConstructor(info.generateConstructor()).build()
).build()
file.writeFile()
}
return true
}
private fun FileSpec.writeFile() {
//文件编译后位置
val kaptKotlinGeneratedDir = processingEnv.options["kapt.kotlin.generated"]
val outputFile = File(kaptKotlinGeneratedDir).apply {
mkdirs()
}
writeTo(outputFile.toPath())
}
private fun bindLayout(element: Element) {
//BindLayout注解的是Class,本身就是TypeElement
val typeElement = element as TypeElement
//一个类一个injectInfo
val className = typeElement.qualifiedName.toString()
var injectInfo = mInjectMaps[className]
if (injectInfo == null) {
injectInfo = InjectInfo(typeElement)
}
typeElement.getAnnotation(BindLayout::class.java).run {
injectInfo.layoutId = value
}
mInjectMaps[className] = injectInfo
}
private fun bindView(element: Element) {
//BindView注解的是变量,element就是VariableElement
val variableElement = element as VariableElement
val typeElement = element.enclosingElement as TypeElement
//一个类一个injectInfo
val className = typeElement.qualifiedName.toString()
var injectInfo = mInjectMaps[className]
if (injectInfo == null) {
injectInfo = InjectInfo(typeElement)
}
variableElement.getAnnotation(BindView::class.java).run {
injectInfo.viewMap[value] = variableElement
}
mInjectMaps[className] = injectInfo
}
private fun bindClickListener(element: Element) {
//OnClick注解的是方法,element就是VariableElement
val variableElement = element as ExecutableElement
val typeElement = element.enclosingElement as TypeElement
//一个类一个injectInfo
val className = typeElement.qualifiedName.toString()
var injectInfo = mInjectMaps[className]
if (injectInfo == null) {
injectInfo = InjectInfo(typeElement)
}
variableElement.getAnnotation(OnClick::class.java).run {
values.forEach {
injectInfo.clickListenerMap[it] = variableElement
}
}
mInjectMaps[className] = injectInfo
}
//把注解类都添加进行,这个方法一看方法名就应该知道干啥的
override fun getSupportedAnnotationTypes(): Set<String> {
return setOf(
BindLayout::class.java.canonicalName,
BindView::class.java.canonicalName,
OnClick::class.java.canonicalName
)
}
override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv)
mLogger = Logger(processingEnv.messager)
mLogger.info("processor init")
}
}
//存储一个Activity文件所有注解数据,并有相应方法生成编译后的文件
class InjectInfo(val element: TypeElement) { var mLogger: Logger? = null //类名 val className: ClassName = element.asClassName() val viewClass: ClassName = ClassName("android.view", "View") //包名 val packageName: String = getPackageName(element).qualifiedName.toString() //布局只有一个id var layoutId: Int = -1 //View 注解数据可能有多个 注意是VariableElement val viewMap = hashMapOf<Int, VariableElement>() //点击事件 注解数据可能有多个 注意是ExecutableElement val clickListenerMap = hashMapOf<Int, ExecutableElement>()
private fun getPackageName(element: Element): PackageElement { var e = element while (e.kind != ElementKind.PACKAGE) { e = e.enclosingElement } return e as PackageElement } fun getClassName(element: Element): ClassName { var elementType = element.asType().asTypeName() return elementType as ClassName }
//自动生成构造方法,主要使用kotlinpoet fun generateConstructor(): FunSpec {
//构造方法,传入activity参数 val builder = FunSpec.constructorBuilder().addParameter("target", className) .addParameter("view", viewClass) if (layoutId != -1) { builder.addStatement("target.setContentView(%L)", layoutId) } viewMap.forEach { (id, variableElement) -> builder.addStatement( "target.%N = view.findViewById(%L)", variableElement.simpleName, id ) } clickListenerMap.forEach { (id, element) -> when (element.parameters.size) { //没有参数 0 -> builder.addStatement( "(view.findViewById(%L) as View).setOnClickListener{target.%N()}" , id ) //一个参数 1 -> { if (getClassName(element.parameters[0]) != viewClass) { mLogger?.error("element.simpleName function parameter error") } builder.addStatement( "(view.findViewById(%L) as View).setOnClickListener{target.%N(it)}" , id, element.simpleName ) } //多个参数错误 else -> mLogger?.error("element.simpleName function parameter error") } } return builder.build() } }
三、app module中引入上面两个lib
//gradle引入
implementation project(‘:lib_annotations‘) kapt project(‘:lib_processor‘)
@BindLayout(R.layout.activity_main)
class MainActivity : AppCompatActivity() {
@BindView(R.id.tv_hello)
lateinit var textView: TextView
@BindView(R.id.bt_click)
lateinit var btClick: Button
private var mClickBtNum = 0
private var mClickTvNum = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main)
//这里第4步内容
BindApi.bind(this)
textView.text = "测试成功......"
btClick.text = "点击0次"
}
@OnClick(R.id.bt_click, R.id.tv_hello)
fun onClick(view: View) {
when (view.id) {
R.id.bt_click -> {
mClickBtNum++
btClick.text = "点击${mClickBtNum}次"
}
R.id.tv_hello -> {
mClickTvNum++
textView.text = "点击文字${mClickTvNum}次"
}
}
}
}
现在就可以直接编译,编译后我们就可以找到编译生成的类MainActivity_BindTest,
import android.view.View
class MainActivity_BindTest(
target: MainActivity,
view: View) {
init {
target.setContentView(2131361820)
target.btClick = view.findViewById(2131165250)
target.textView = view.findViewById(2131165360)
(view.findViewById(2131165250) as View).setOnClickListener { target.onClick(it) }
(view.findViewById(2131165360) as View).setOnClickListener { target.onClick(it) }
}
}
这里当然还不能用,因为我们没有把MainActivity_BindTest和MainActivity关联上。
四、创建App module(lib_api)
object BindApi {
//类似ButterKnife方法
fun bind(target: Activity) {
val sourceView = target.window.decorView
createBinding(target, sourceView)
}
private fun createBinding(target: Activity, source: View) {
val targetClass = target::class.java
var className = targetClass.name
try {
//获取类名
val bindingClass = targetClass.classLoader!!.loadClass(className + "_BindTest")
//获取构造方法
val constructor = bindingClass.getConstructor(targetClass, View::class.java)
//向方法中传入数据activity和view
constructor.newInstance(target, source)
} catch (e: ClassNotFoundException) {
e.printStackTrace()
} catch (e: NoSuchMethodException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
} catch (e: InstantiationException) {
e.printStackTrace()
} catch (e: InvocationTargetException) {
e.printStackTrace()
}
}
}
并在app中引用
implementation project(‘:lib_api‘)
五、总结
流程还是比较简单,创建annotation、processor、lib_api 3个module,我们打包时并不需要processor包,它的目的仅仅是生成相应的文件代码。
注意点:
1、annotation 和processor要引入
apply plugin: ‘kotlin‘
2、编译时打印使用Messager,注意JDK8打印NOTE无法显示
3、lib_api 文件在反射时要主义和processor对应,修改时注意同步修改等
有用的话加个关注哦!!!
以上是关于Kotlin编译时注解,简单实现ButterKnife的主要内容,如果未能解决你的问题,请参考以下文章