安卓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.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方法

  1. 按照类整理属性,获取类的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)
        }
  1. 整理后根据一个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
  }
}

Demo代码链接

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值