失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > 安卓apt开发kotlin 利用编译时注解生成源码Demo

安卓apt开发kotlin 利用编译时注解生成源码Demo

时间:2022-10-12 08:20:18

相关推荐

安卓apt开发kotlin 利用编译时注解生成源码Demo

项目中要减少反射,提高性能,可以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.0sourceCompatibility = JavaVersion.VERSION_1_8targetCompatibility = 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')//注意 一定要kaptkapt 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 {@UiThreadfun 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? = nullprivate 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? = nullprivate var mElementUtils: Elements? = nulloverride fun init(processingEnv: ProcessingEnvironment?) {super.init(processingEnv)mFiler = processingEnv?.filermElementUtils = 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.enclosingElementvar 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 //Elementval viewBindElements = it.value //ArrayList<Element>.......}

接下去是使用kotlinpoet生成源码,如果用文件流,可以忽略以下代码:

kotlinpoet里面有一个ClassName,通过它可以拿到很多Class的属性。比如,我们获取apt下接口的方式:

val interfaceClassName = ClassName("and99.apt", "Unbinder")

and99.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).valueconstructorMethodBuilder.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为KModifierval 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 and99.aptimport androidx.`annotation`.CallSuperimport kotlin.Unitpublic final class MainActivityViewBinding(private var target: MainActivity?) : Unbinder {init {target?.helloWorld = Utils.findViewById(target,2131230886)}@CallSuperpublic override fun unbind(): Unit {// 销毁this.target = targetif (target == null) throw IllegalStateException("Binding already cleares.")target = nulltarget?.helloWorld = null}}

Demo代码链接

如果觉得《安卓apt开发kotlin 利用编译时注解生成源码Demo》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。