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的更多相关文章

  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. Harbinger vs Sciencepal

    Harbinger vs Sciencepal 题意:给你n对人, 每一对都有2个人,每个人分别有一个值, 现在将每队人拆开塞入2组,要求分完这n对人之后,2个组的差值最小. 题解:将每队人的差值算出 ...

  2. codeforces 820 D. Mister B and PR Shifts(思维)

    题目链接:http://codeforces.com/contest/820/problem/D 题意:求.有一种操作 k = 0: shift p1, p2, ... pn, k = 1: shif ...

  3. Python 之父的解析器系列之五:左递归 PEG 语法

    原题 | Left-recursive PEG grammars 作者 | Guido van Rossum(Python之父) 译者 | 豌豆花下猫("Python猫"公众号作者 ...

  4. Spring Boot与Spring MVC集成启动过程源码分析

    开源项目推荐 Pepper Metrics是我与同事开发的一个开源工具(https://github.com/zrbcool/pepper-metrics),其通过收集jedis/mybatis/ht ...

  5. FreeSql (二十三)分组、聚合

    IFreeSql fsql = new FreeSql.FreeSqlBuilder() .UseConnectionString(FreeSql.DataType.MySql, "Data ...

  6. JDK8时间工具类

    JDK8添加了java.time包,提供了很多方便.用得比较多的几个类:Instant 在时间线上模拟单个瞬时点Duration 以秒和纳秒为单位模拟一个数量或时间量.可以使用其他基于持续时间的单位访 ...

  7. IOCAutofac与ORMEntityFramwork的联系--单例模式

    在你阅读之前默认你已经理解了IOC.DI.ORM以及autofac和EF的使用 在我最近写项目的时候我在单步调试时偶然发现的一个问题 先说明我的项目使用.NET MVC 三层架构,运用IOC Auto ...

  8. 5.Sentinel源码分析—Sentinel如何实现自适应限流?

    Sentinel源码解析系列: 1.Sentinel源码分析-FlowRuleManager加载规则做了什么? 2. Sentinel源码分析-Sentinel是如何进行流量统计的? 3. Senti ...

  9. MYSQL之概念基础篇

    1数据库概述 1.1 数据管理技术的产生和发展 数据库技术是应数据库管理任务的需要而产生的.20世纪50年代中期以前,计算机主要是用于科学计算.当时的硬件状况是,外存只有纸带.卡片.磁带,没有磁盘等可 ...

  10. Java 自定义注解 校验指定字段对应数据库内容重复

    一.前言 在项目中,某些情景下我们需要验证编码是否重复,账号是否重复,身份证号是否重复等... 而像验证这类代码如下: 那么有没有办法可以解决这类似的重复代码量呢? 我们可以通过自定义注解校验的方式去 ...