ButterKnife在之前的Android开发中还是比较热门的工具,帮助Android开发者减少代码编写,而且看起来更加的舒适,于是简单实现一下ButterKnife,相信把下面的代码都搞懂,看ButterKnife的难度就小很多。

今天实现的是编译时注解,其实运行时注解也一样能实现ButterKnife的效果,但是相对于编译时注解,运行时注解会更耗性能一些,主要是由于运行时注解大量使用反射。

一、创建java library(lib_annotations)

我这里创建3个annotation放在3个文件中

  1. //绑定layout
    @Target(AnnotationTarget.CLASS)
  2. @Retention(AnnotationRetention.BINARY)
  3. annotation class BindLayout(val value: Int = -1)

  4. //绑定view
  5. @Target(AnnotationTarget.FIELD)
  6. @Retention(AnnotationRetention.RUNTIME)
  7. annotation class BindView (val value:Int = -1)

  8. //点击注解
  9. @Target(AnnotationTarget.FUNCTION)
  10. @Retention(AnnotationRetention.BINARY)
  11. annotation class OnClick (vararg val values:Int)

Kotlin对编译时注解时Retention 并没有太多的要求,一般我们使用AnnotationRetention.BINARY或者SOURCE,但是我发现ButterKnife用的是Runtime,测试也可以。

但具体为什么用,不是特别明白,自己认为是AnnotationRetention.RUNTIME基本包含了BINARY或者SOURCE的功能,还支持反射。

二、创建java library(lib_processor)

  1. @AutoService(Processor::class)
  2. @SupportedSourceVersion(SourceVersion.RELEASE_8)
  3. class BindProcessor : AbstractProcessor() {
  4. companion object {
  5. private const val PICK_END = "_BindTest"
  6. }
  7.  
  8. private lateinit var mLogger: Logger
  9. //存储类文件数据
  10. private val mInjectMaps = hashMapOf<String, InjectInfo>()

  11. //必须实现方法
  12. override fun process(
  13. annotations: MutableSet<out TypeElement>?,
  14. roundEnv: RoundEnvironment
  15. ): Boolean {
  16. //里面就要生成我们需要的文件
  17.  
  18. roundEnv.getElementsAnnotatedWith(BindLayout::class.java).forEach {
  19. bindLayout(it)
  20. }
  21.  
  22. roundEnv.getElementsAnnotatedWith(BindView::class.java).forEach {
  23. bindView(it)
  24. }
  25.  
  26. roundEnv.getElementsAnnotatedWith(OnClick::class.java).forEach {
  27. bindClickListener(it)
  28. }
  29.  
  30. mInjectMaps.forEach { (name, info) ->
  31. //这里生成文件
  32. val file= FileSpec.builder(info.packageName, info.className.simpleName + PICK_END)
  33. .addType(
  34. TypeSpec.classBuilder(info.className.simpleName + PICK_END)
  35. .primaryConstructor(info.generateConstructor()).build()
  36. ).build()
  37.  
  38. file.writeFile()
  39. }
  40.  
  41. return true
  42. }
  43.  
  44. private fun FileSpec.writeFile() {
  45. //文件编译后位置
  46. val kaptKotlinGeneratedDir = processingEnv.options["kapt.kotlin.generated"]
  47. val outputFile = File(kaptKotlinGeneratedDir).apply {
  48. mkdirs()
  49. }
  50. writeTo(outputFile.toPath())
  51. }
  52.  
  53. private fun bindLayout(element: Element) {
  54. //BindLayout注解的是Class,本身就是TypeElement
  55. val typeElement = element as TypeElement
  56. //一个类一个injectInfo
  57. val className = typeElement.qualifiedName.toString()
  58. var injectInfo = mInjectMaps[className]
  59. if (injectInfo == null) {
  60. injectInfo = InjectInfo(typeElement)
  61. }
  62.  
  63. typeElement.getAnnotation(BindLayout::class.java).run {
  64. injectInfo.layoutId = value
  65. }
  66.  
  67. mInjectMaps[className] = injectInfo
  68. }
  69.  
  70. private fun bindView(element: Element) {
  71. //BindView注解的是变量,element就是VariableElement
  72. val variableElement = element as VariableElement
  73. val typeElement = element.enclosingElement as TypeElement
  74. //一个类一个injectInfo
  75. val className = typeElement.qualifiedName.toString()
  76. var injectInfo = mInjectMaps[className]
  77. if (injectInfo == null) {
  78. injectInfo = InjectInfo(typeElement)
  79. }
  80.  
  81. variableElement.getAnnotation(BindView::class.java).run {
  82. injectInfo.viewMap[value] = variableElement
  83. }
  84.  
  85. mInjectMaps[className] = injectInfo
  86. }
  87.  
  88. private fun bindClickListener(element: Element) {
  89. //OnClick注解的是方法,element就是VariableElement
  90. val variableElement = element as ExecutableElement
  91. val typeElement = element.enclosingElement as TypeElement
  92. //一个类一个injectInfo
  93. val className = typeElement.qualifiedName.toString()
  94. var injectInfo = mInjectMaps[className]
  95. if (injectInfo == null) {
  96. injectInfo = InjectInfo(typeElement)
  97. }
  98.  
  99. variableElement.getAnnotation(OnClick::class.java).run {
  100. values.forEach {
  101. injectInfo.clickListenerMap[it] = variableElement
  102. }
  103. }
  104.  
  105. mInjectMaps[className] = injectInfo
  106. }

  107. //把注解类都添加进行,这个方法一看方法名就应该知道干啥的
  108. override fun getSupportedAnnotationTypes(): Set<String> {
  109. return setOf(
  110. BindLayout::class.java.canonicalName,
  111. BindView::class.java.canonicalName,
  112. OnClick::class.java.canonicalName
  113. )
  114. }
  115.  
  116. override fun init(processingEnv: ProcessingEnvironment) {
  117. super.init(processingEnv)
  118. mLogger = Logger(processingEnv.messager)
  119. mLogger.info("processor init")
  120. }
  121. }
  1. //存储一个Activity文件所有注解数据,并有相应方法生成编译后的文件
    class InjectInfo(val element: TypeElement) {
  2.  
  3. var mLogger: Logger? = null
  4. //类名
  5. val className: ClassName = element.asClassName()
  6. val viewClass: ClassName = ClassName("android.view", "View")
  7. //包名
  8. val packageName: String = getPackageName(element).qualifiedName.toString()
  9.  
  10. //布局只有一个id
  11. var layoutId: Int = -1
  12. //View 注解数据可能有多个 注意是VariableElement
  13. val viewMap = hashMapOf<Int, VariableElement>()
  14. //点击事件 注解数据可能有多个 注意是ExecutableElement
  15. val clickListenerMap = hashMapOf<Int, ExecutableElement>()

  16. private fun getPackageName(element: Element): PackageElement {
  17. var e = element
  18. while (e.kind != ElementKind.PACKAGE) {
  19. e = e.enclosingElement
  20. }
  21. return e as PackageElement
  22. }
  23.  
  24. fun getClassName(element: Element): ClassName {
  25. var elementType = element.asType().asTypeName()
  26.  
  27. return elementType as ClassName
  28. }

  29. //自动生成构造方法,主要使用kotlinpoet
  30. fun generateConstructor(): FunSpec {
    //构造方法,传入activity参数
  31. val builder = FunSpec.constructorBuilder().addParameter("target", className)
  32. .addParameter("view", viewClass)
  33.  
  34. if (layoutId != -1) {
  35. builder.addStatement("target.setContentView(%L)", layoutId)
  36. }
  37.  
  38. viewMap.forEach { (id, variableElement) ->
  39. builder.addStatement(
  40. "target.%N = view.findViewById(%L)",
  41. variableElement.simpleName,
  42. id
  43. )
  44. }
  45.  
  46. clickListenerMap.forEach { (id, element) ->
  47.  
  48. when (element.parameters.size) {
  49. //没有参数
  50. 0 -> builder.addStatement(
  51. "(view.findViewById(%L) as View).setOnClickListener{target.%N()}"
  52. , id
  53. )
  54. //一个参数
  55. 1 -> {
  56. if (getClassName(element.parameters[0]) != viewClass) {
  57. mLogger?.error("element.simpleName function parameter error")
  58. }
  59. builder.addStatement(
  60. "(view.findViewById(%L) as View).setOnClickListener{target.%N(it)}"
  61. , id, element.simpleName
  62. )
  63. }
  64. //多个参数错误
  65. else -> mLogger?.error("element.simpleName function parameter error")
  66. }
  67.  
  68. }
  69.  
  70. return builder.build()
  71. }
  72.  
  73. }

三、app module中引入上面两个lib

  1. //gradle引入
    implementation project(':lib_annotations')
  2. kapt project(':lib_processor')
  1. @BindLayout(R.layout.activity_main)
  2. class MainActivity : AppCompatActivity() {
  3.  
  4. @BindView(R.id.tv_hello)
  5. lateinit var textView: TextView
  6. @BindView(R.id.bt_click)
  7. lateinit var btClick: Button
  8.  
  9. private var mClickBtNum = 0
  10. private var mClickTvNum = 0
  11. override fun onCreate(savedInstanceState: Bundle?) {
  12. super.onCreate(savedInstanceState)
  13. // setContentView(R.layout.activity_main)
  14. //这里第4步内容
  15. BindApi.bind(this)
  16.  
  17. textView.text = "测试成功......"
  18. btClick.text = "点击0次"
  19. }
  20.  
  21. @OnClick(R.id.bt_click, R.id.tv_hello)
  22. fun onClick(view: View) {
  23. when (view.id) {
  24. R.id.bt_click -> {
  25. mClickBtNum++
  26. btClick.text = "点击${mClickBtNum}次"
  27. }
  28. R.id.tv_hello -> {
  29. mClickTvNum++
  30. textView.text = "点击文字${mClickTvNum}次"
  31. }
  32. }
  33. }
  34. }

现在就可以直接编译,编译后我们就可以找到编译生成的类MainActivity_BindTest,

  1. import android.view.View
  2.  
  3. class MainActivity_BindTest(
  4. target: MainActivity,
  5. view: View) {
  6. init {
  7. target.setContentView(2131361820)
  8. target.btClick = view.findViewById(2131165250)
  9. target.textView = view.findViewById(2131165360)
  10. (view.findViewById(2131165250) as View).setOnClickListener { target.onClick(it) }
  11. (view.findViewById(2131165360) as View).setOnClickListener { target.onClick(it) }
  12. }
  13. }

这里当然还不能用,因为我们没有把MainActivity_BindTest和MainActivity关联上。

四、创建App module(lib_api)

  1. object BindApi {
  2.  
  3. //类似ButterKnife方法
  4. fun bind(target: Activity) {
  5. val sourceView = target.window.decorView
  6. createBinding(target, sourceView)
  7. }
  8.  
  9. private fun createBinding(target: Activity, source: View) {
  10. val targetClass = target::class.java
  11. var className = targetClass.name
  12. try {
  13. //获取类名
  14. val bindingClass = targetClass.classLoader!!.loadClass(className + "_BindTest")
  15. //获取构造方法
  16. val constructor = bindingClass.getConstructor(targetClass, View::class.java)
  17. //向方法中传入数据activity和view
  18. constructor.newInstance(target, source)
  19. } catch (e: ClassNotFoundException) {
  20. e.printStackTrace()
  21. } catch (e: NoSuchMethodException) {
  22. e.printStackTrace()
  23. } catch (e: IllegalAccessException) {
  24. e.printStackTrace()
  25. } catch (e: InstantiationException) {
  26. e.printStackTrace()
  27. } catch (e: InvocationTargetException) {
  28. e.printStackTrace()
  29. }
  30. }
  31. }

并在app中引用

  1. implementation project(':lib_api')

五、总结

流程还是比较简单,创建annotation、processor、lib_api 3个module,我们打包时并不需要processor包,它的目的仅仅是生成相应的文件代码。

注意点:

1、annotation 和processor要引入

  1. apply plugin: 'kotlin'

2、编译时打印使用Messager,注意JDK8打印NOTE无法显示

3、lib_api 文件在反射时要注意和processor对应,修改时注意同步修改等

有用的话加个关注哦!!!

代码

Kotlin编译时注解,简单实现ButterKnife的更多相关文章

  1. 使用编译时注解简单实现类似 ButterKnife 的效果

    这篇文章是学习鸿洋前辈的 Android 如何编写基于编译时注解的项目 的笔记,用于记录我的学习收获. 读完本文你将了解: 什么是编译时注解 APT 编译时注解如何使用与编写 举个例子 思路 创建注解 ...

  2. Android 编译时注解解析框架

    2.注解 说道注解,竟然还有各种分类,得,这记不住,我们从注解的作用来反推其分类,帮助大家记忆,然后举例强化大家的记忆,话说注解的作用: 1.标记一些信息,这么说可能太抽象,那么我说,你见过@Over ...

  3. Android 打造编译时注解解析框架 这只是一个开始

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43452969 ,本文出自:[张鸿洋的博客] 1.概述 记得很久以前,写过几篇博客 ...

  4. 利用APT实现Android编译时注解

    摘要: 一.APT概述 我们在前面的java注解详解一文中已经讲过,可以在运行时利用反射机制运行处理注解.其实,我们还可以在编译时处理注解,这就是不得不说官方为我们提供的注解处理工具APT (Anno ...

  5. java 编译时注解框架 lombok-ex

    lombok-ex lombok-ex 是一款类似于 lombok 的编译时注解框架. 编译时注,拥有运行时注解的便利性,和无任何损失的性能. 主要补充一些 lombok 没有实现,且自己会用到的常见 ...

  6. lombok编译时注解@Slf4j的使用及相关依赖包

    slf4j是一个日志门面模式的框架,只对调用者开放少量接口用于记录日志 主要接口方法有 debug warn info error trace 在idea中可以引入lombok框架,使用@Slf4j注 ...

  7. Java 进阶巩固:什么是注解以及运行时注解的使用

    这篇文章 2016年12月13日星期二 就写完了,当时想着等写完另外一篇关于自定义注解的一起发.结果没想到这一等就是半年多 - -. 有时候的确是这样啊,总想着等条件更好了再干,等准备完全了再开始,结 ...

  8. apt 根据注解,编译时生成代码

    apt: @Retention后面的值,设置的为CLASS,说明就是编译时动态处理的.一般这类注解会在编译的时候,根据注解标识,动态生成一些类或者生成一些xml都可以,在运行时期,这类注解是没有的~~ ...

  9. 自定义注解之运行时注解(RetentionPolicy.RUNTIME)

    对注解概念不了解的可以先看这个:Java注解基础概念总结 前面有提到注解按生命周期来划分可分为3类: 1.RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成clas ...

随机推荐

  1. 牛客-2018多校算法第五场D-集合问题+并查集

    集合问题 题意: 给你a,b和n个数p[i],问你如何分配这n个数给A,B集合,并且满足: 若x在集合A中,则a-x必须也在集合A中. 若x在集合B中,则b-x必须也在集合B中. 思路:并查集操作,自 ...

  2. Aizu-2249 Road Construction(dijkstra求最短路)

    Aizu - 2249 题意:国王本来有一个铺路计划,后来发现太贵了,决定删除计划中的某些边,但是有2个原则,1:所有的城市必须能达到. 2:城市与首都(1号城市)之间的最小距离不能变大. 并且在这2 ...

  3. CodeForces 1082 G Petya and Graph 最大权闭合子图。

    题目传送门 题意:现在有一个图,选择一条边,会把边的2个顶点也选起来,最后会的到一个边的集合 和一个点的集合 , 求边的集合 - 点的集合最大是多少. 题解:裸的最大权闭合子图. 代码: #inclu ...

  4. Codeforces 939 D Love Rescue

    Love Rescue 题意:Valya 和 Tolya 是一对情侣, 他们的T恤和头巾上都有小写字母,但是女朋友嫌弃男朋友上T恤上的字不和她的头巾上的字一样,就很生气, 然后来了一个魔法师, 它可以 ...

  5. Halloween treats HDU 1808 鸽巢(抽屉)原理

    Halloween treats Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) ...

  6. Wamp 新增php版本 教程

    a.php版本下载:https://windows.php.net/download b.如果是apache环境下请认准  Thread Safe  版本 下载解压zip c.调整文件名为 php7. ...

  7. Python 70行代码实现简单算式计算器

    描述:用户输入一系列算式字符串,程序返回计算结果. 要求:不使用eval.exec函数. 实现思路:找到当前字符串优先级最高的表达式,在算术运算中,()优先级最高,则取出算式最底层的(),再进行加减乘 ...

  8. MySQL索引原理及SQL优化

    目录 索引(Index) 索引的原理 b+树 MySQL如何使用索引 如何优化 索引虽好,不可滥用 如何验证索引使用情况? SQL优化 explain查询执行计划 id select_type tab ...

  9. TestNG(八) 类分组测试

    package com.course.testng.groups; import org.testng.annotations.Test; @Test(groups = "stu" ...

  10. 007:CSS字体图标

    目录 理论 一:字体图标 图片是有诸多优点的,但是缺点很明显,比如图片不但增加了总文件的大小,还增加了很多额外的"http请求",这都会大大降低网页的性能的.更重要的是图片不能很好 ...