项目中要减少反射,提高性能,可以apt或是aop。网上有很多java apt的文章,可是利用kotlin文章比较少,有的也不够详细。
Demo 仿著名的butterknife实现一个简单的View绑定
编译时注解核心三个模块,一个安卓库(实现一些需要的功能),一个java compiler库(实现编译时生成代理),一个java annotaions库(注解库)。
架构
我们需要新建三个模块
依赖
compiler 增加kapt插件和依赖,如下:
plugins {
id 'java'
id 'java-library'
id 'kotlin'
id 'kotlin-kapt'
}
java {
//默认创建为1.7,一定要改1.8,不然无法导入com.squareup:kotlinpoet:1.8.0
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation project(path: ':apt-annotations')
implementation "com.google.auto.service:auto-service:1.0"
kapt "com.google.auto.service:auto-service:1.0"
implementation "com.squareup:kotlinpoet:1.8.0"
}
关键是JavaVersion.VERSION_1_8 ,默认1.7,浪费了我几个小时排查这个错误。
kotlinpoet是辅助生产代理的工具,如果不用可以直接用io流写文件,优点类似jsp。kotlinpoet官方API说明文档
app模块整加依赖,如下:
plugins {
.......
id 'kotlin-kapt'
}
android{
.......
}
dependencies {
......
implementation project(path: ':apt')
kapt project(path: ':apt-annotations')
implementation project(path: ':apt-annotations')
//注意 一定要kapt
kapt project(path: ':apt-compiler')
.......
}
代码
因为是demo 我们只实现一个bindview,所以注解模块最简单,写一个注解
/**
* 用来注入view
* @author markrenChina
*/
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.BINARY)
annotation class BindView(
val value: Int
)
apt模块是一些具体的功能,具体到我们模仿的butterknife,是一个Unbinder接口,一个静态方法(反射创建生成代码的实例),为了减少代码还有一个工具类,如下:
Apt.kt
object Apt {
fun bind(activity: Activity): Unbinder {
//反射创建实例
try {
val bindClassName: Class<out Unbinder> =
Class.forName("${activity.javaClass.name}ViewBinding") as Class<out Unbinder>
//构造函数
val bindConstructor: Constructor<out Unbinder> =
bindClassName.getDeclaredConstructor(activity.javaClass)
return bindConstructor.newInstance(activity)
} catch (e: Exception) {
e.printStackTrace()
}
return Unbinder.EMPTY
}
}
butterknife 源码为了保证后面代码正确,保证返回的不是一个空类型。但是kotlin可空类型可以很好的处理这个问题,这里我们按照butterknife的源码
Unbinder.kt
interface Unbinder {
@UiThread
fun unbind()
companion object {
val EMPTY: Unbinder = object : Unbinder {
override fun unbind() {}
}
}
}
Utils.kt
object Utils {
fun <T : View> findViewById(activity: Activity?, viewId: Int): T? {
return activity?.findViewById(viewId) as T?
}
}
app模块:
MainActivity.kt
class MainActivity : AppCompatActivity() {
@BindView(R.id.hello_world)
var helloWorld: TextView? = null
private lateinit var mUnbinder: Unbinder
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mUnbinder = Apt.bind(this)
helloWorld?.text = "hello markrenChina!!"
}
override fun onDestroy() {
mUnbinder.unbind()
super.onDestroy()
}
}
布局送的TextView加一个id,hello_world
compiler模块
代码比较多,一个功能一个功能介绍
新建一个类继承AbstractProcessor来实现apt,为了避免写配置,需要@AutoService(Processor::class)注解
@AutoService(Processor::class)
class AptProcessor : AbstractProcessor() {}
一定要复写的process是处理的关键方法。
在初始化时我们需要拿到2个全局变量
private var mFiler: Filer? = null
private var mElementUtils: Elements? = null
override fun init(processingEnv: ProcessingEnvironment?) {
super.init(processingEnv)
mFiler = processingEnv?.filer
mElementUtils = processingEnv?.elementUtils
}
Filer是javax包下注解处理的文件接口
mElementUtils是大名鼎鼎的javax.lang包下的,后面用来处理类相关的。
然后是一些模板代理,大致所有的处理都是这么写的:
//指定处理的版本
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latestSupported()
}
//给到需要处理的注解
override fun getSupportedAnnotationTypes(): MutableSet<String> {
val types: LinkedHashSet<String> = LinkedHashSet()
getSupportedAnnotations().forEach { clazz: Class<out Annotation> ->
types.add(clazz.canonicalName)
}
return types
}
private fun getSupportedAnnotations(): Set<Class<out Annotation>> {
val annotations: LinkedHashSet<Class<out Annotation>> = LinkedHashSet()
// 需要解析的自定义注解
annotations.add(BindView::class.java)
return annotations
}
核心process方法:
- 按照类整理属性,获取类的Element (element.enclosingElement还是Element)
override fun process(
p0: MutableSet<out TypeElement>?,
roundEnvironment: RoundEnvironment?
): Boolean {
//解析属性 activity ->list<Element>
val elementMap = LinkedHashMap<Element, ArrayList<Element>>()
// 有注解就会进来
roundEnvironment?.getElementsAnnotatedWith(BindView::class.java)?.forEach { element ->
//按照 类 整理 属性
val enclosingElement = element.enclosingElement
var viewBindElements = elementMap[enclosingElement]
if (viewBindElements == null) {
viewBindElements = ArrayList()
elementMap[enclosingElement] = viewBindElements
}
viewBindElements.add(element)
}
- 整理后根据一个key(activity)生成一个viewBind.kt,所有对map进行循环。
// 生成代码
elementMap.entries.forEach {
val clazz = it.key //Element
val viewBindElements = it.value //ArrayList<Element>
.......
}
接下去是使用kotlinpoet生成源码,如果用文件流,可以忽略以下代码:
kotlinpoet里面有一个ClassName,通过它可以拿到很多Class的属性。比如,我们获取apt下接口的方式:
val interfaceClassName = ClassName("com.ccand99.apt", "Unbinder")
com.ccand99.apt是包名,作为android library,app引用时包名时固定的,我们可以写死。
获取Element包名的方式
//动态获取包名
val packageName = mElementUtils?.getPackageOf(clazz)?.qualifiedName?.toString()
?: throw RuntimeException("无法获取包名")
这个包名很重要,知道这个包名可以获取包下类,我们生成类的目录等等。比如,我们activity的获取
val activityStr = clazz.simpleName.toString()
val activityKtClass = ClassName(packageName, activityStr)
获取activity的ClassName,我们才能去拼接kotlin中的 参数名:参数类型。例如,activity:MainActivity。如果用字符串,MainActivity是可以直接用element.simpleName.toString获得,直接:字符串,生成的源码也没有问题。但是,使用ClassName,kotlinpoet会自动帮助import的。当然javapoet也是有的,java有太多的类型 参数名的格式,还是直接用字符串比较方面。
注意的几个方法:
设置类属性:
val property = PropertySpec.builder("target", activityKtClass.copy(nullable = true))
.initializer("target")
.mutable()//var 不加val
.addModifiers(KModifier.PRIVATE)
设置构造函数
//构造构造函数
val constructorMethodBuilder = FunSpec.constructorBuilder()
.addParameter("target", activityKtClass.copy(nullable = true))//java 不需要传类型 可空
viewBindElements.forEach { element ->
val resId = element.getAnnotation(BindView::class.java).value
constructorMethodBuilder.addStatement("target?.${element.simpleName} = %T.findViewById(target,$resId)",findByIdUtilsClass)
}
占位用%,跟javapoet不一样
构造unbind方法
//生成类方法
val unbindMethodBuilder = FunSpec.builder("unbind")
.addAnnotation(callSuperClassName)
.addModifiers(KModifier.OVERRIDE)
.addComment("销毁")
.addStatement("this.target = target")
.addStatement("if (target == null) throw IllegalStateException(\"Binding already cleares.\")")
.addStatement("target = null")
viewBindElements.forEach { element ->
unbindMethodBuilder.addStatement("target?.${element.simpleName} = null")
}
构造类:
// 构造类
// public final 类kotlin为KModifier
val clazzBuilder = TypeSpec.classBuilder("${clazz.simpleName}ViewBinding")
.addModifiers(KModifier.FINAL, KModifier.PUBLIC)
//构造函数
.primaryConstructor(constructorMethodBuilder.build())
.addProperty(property.build())
.addSuperinterface(interfaceClassName)
clazzBuilder.addFunction(unbindMethodBuilder.build())
生成类文件
//生成类文件
val classFile = FileSpec.builder(packageName, "${clazz.simpleName}ViewBinding")
.addType(clazzBuilder.build())
.addComment("测试 自动生成")
.build()
//classFile.writeTo(System.out)
//输出的文件映射
try {
mFiler?.let { filer -> classFile.writeTo(filer) }
} catch (e: IOException) {
println(e.message)
}
生成源代理:
// 测试 自动生成
package com.ccand99.apt
import androidx.`annotation`.CallSuper
import kotlin.Unit
public final class MainActivityViewBinding(
private var target: MainActivity?
) : Unbinder {
init {
target?.helloWorld = Utils.findViewById(target,2131230886)
}
@CallSuper
public override fun unbind(): Unit {
// 销毁
this.target = target
if (target == null) throw IllegalStateException("Binding already cleares.")
target = null
target?.helloWorld = null
}
}