本文为稀土技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

apt&kapt&ksp

在接触元编程的时分,apt估量是许多开发者最早接触的,APT即为Annotation Processing Tool,它是javac的一个东西。如其字面意思,APT能够用来在编译时扫描和处理注解,注解处理器。在Arouter或者dagger等开源结构中遍及运用,当咱们想要生成某些模版代码的时分,就能够经过apt去生成。

相同的,当kotlin语言逐步风靡全国际后,一起也伴随着元编程的需求,因而kapt上台,它借助了apt,实现了处理kotlin注解的才能与生成代码的才能,可是随着时间的推移,咱们发现kapt在编译生成代码的效率往往没有apt高,原因是kapt经过了以下几个阶段

image.png
能够看到,kotlin源码会被编译成一个叫javastubs的,从而被纳入虚拟机国际,因而多了这么一个过程,往往在kotlin与java混编的环境下形成明显的编译瓶颈,此时,针对kotlin符号的编译插件出来了,它便是kcp(Kotlin Compiler Plugin)

image.png
它在kotlinc的编译阶段,供给了各种办法给开发者去修正kotlin符号。可是由于其难度的杂乱,kcp很长一段时间处于beta阶段,一起由于直接触及了编译过程,其运用的杂乱度也随之上升,而咱们聪明的工程师,在kcp的基础上进行了愈加才能的聚合,对kotlin符号的处理进一步进行了封装,它便是咱们今天要讲的主角ksp,ksp在代码生成上,进行了更多的过程精简,如图
image.png

因而ksp在编译速度上得到了较大的提升(20%以上),因而咱们下面来认识一下ksp的一起,也尝试用ksp生成一些咱们自己的代码

ksp

一个ksp插件一般由以下部分组成SymbolProcessorProvider,SymbolProcessor,自定义处理逻辑

SymbolProcessorProvider

/**
 * [SymbolProcessorProvider] is the interface used by plugins to integrate into Kotlin Symbol Processing.
 */
fun interface SymbolProcessorProvider {
    /**
     * Called by Kotlin Symbol Processing to create the processor.
     */
    fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}

SymbolProcessorProvider是环境的供给者,主要为咱们符号处理器SymbolProcessor供给了必要的环境(SymbolProcessorEnvironment),其间SymbolProcessorEnvironment里边有咱们需求编译时用到的各种内容

class SymbolProcessorEnvironment(
    val options: Map<String, String>,
    val kotlinVersion: KotlinVersion,
    val codeGenerator: CodeGenerator,
    val logger: KSPLogger,
    val apiVersion: KotlinVersion,
    val compilerVersion: KotlinVersion,
    val platforms: List<PlatformInfo>,
) {
    // For compatibility with KSP 1.0.2 and earlier
    constructor(
        options: Map<String, String>,
        kotlinVersion: KotlinVersion,
        codeGenerator: CodeGenerator,
        logger: KSPLogger
    ) : this(
        options,
        kotlinVersion,
        codeGenerator,
        logger,
        kotlinVersion,
        kotlinVersion,
        emptyList()
    )
}

比方codeGenerator,用于用于生成与管理文件。logger供给了编译时打印log的入口等等。在运用时,咱们需求承继SymbolProcessorProvider

class MySymbolProcessorProvider: SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
         return MyProcessor(environment.codeGenerator, environment.logger)
    }
}

一起SymbolProcessorProvider是根据spi服务机制的,编写过spi机制的小伙伴或许比较了解,咱们需求在resource/META-INF/services下定义一个com.google.devtools.ksp.processing.SymbolProcessorProvider文件 (spi机制中露出的接口)
image.png
内容便是咱们实现接口的类,本比方是(MySymbolProcessorProvider)

image.png

SymbolProcessor

SymbolProcessor便是咱们真实处理“符号”的地方

interface SymbolProcessor {
    /**
     * Called by Kotlin Symbol Processing to run the processing task.
     *
     * @param resolver provides [SymbolProcessor] with access to compiler details such as Symbols.
     * @return A list of deferred symbols that the processor can't process. Only symbols that can't be processed at this round should be returned. Symbols in compiled code (libraries) are always valid and are ignored if returned in the deferral list.
     */
    fun process(resolver: Resolver): List<KSAnnotated>
    /**
     * Called by Kotlin Symbol Processing to finalize the processing of a compilation.
     */
    fun finish() {}
    /**
     * Called by Kotlin Symbol Processing to handle errors after a round of processing.
     */
    fun onError() {}
}

它是一个接口,关键的是process这个办法,这个办法供给了一个Resolver类型的参数,Resolver里边供给了非常多符号的处理办法,比方咱们常用的getSymbolsWithAnnotation(获取带有某个注解的符号),getClassDeclarationByName(获取指定称号的class符号)等等,这儿就不逐个举例。

咱们以一个比方动身,假定咱们要找到某个注解润饰的办法符号

annotation class TestFind()
class MyProcessor(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger
) : SymbolProcessor {
    override fun process(resolver: Resolver): List<KSAnnotated> {
        val mySymbol = resolver.getSymbolsWithAnnotation(TestFind::class.qualifiedName!!)
        val ret = mySymbol.filter { !it.validate() }.toList()
        val list = mySymbol.filter {
            it is KSFunctionDeclaration
        }.map {
            it as KSFunctionDeclaration
        }.toList()
        logger.warn("list is ${list.toList()}")
        交给自定义逻辑处理
        MyFuncHandler.generate(codeGenerator,logger,list)
        return ret
    }
}

值得留意的是,咱们有一个回来值,这儿回来的是一个list调集,代表着那些不需求被修正的符号,由于咱们注解润饰的符号不一定都有效

Returns:
A list of deferred symbols that the processor can’t process. Only symbols that can’t be processed at this round should be returned. Symbols in compiled code (libraries) are always valid and are ignored if returned in the deferral list.

这儿咱们经过logger.warn(“list is ${list.toList()}”)打印一下所有的符号list(可在build控制台看到输出,假如直接用print则看不到)。

或许看到这儿小伙伴有点懵逼,这儿咱们选用了filter函数去找到KSFunctionDeclaration类型的list,那么KSFunctionDeclaration是个啥,其实这个类便是代表了函数符号的声明,相对应的,还有类符号声明,特点符号声明等等,其都承继于KSDeclaration

image.png

自定义处理逻辑

经过上面的过程,咱们能拿到相关的符号对应的类了,比方本比方中,咱们拿到了所以用@TestFind注解润饰的办法,分别是fun1,fun2


class TestClass {
    @TestFind
    fun fun1(){
    }
    @TestFind
    fun fun2(){
    }
}

咱们这儿使用注解生成一个简单的逻辑吧,便是用 @TestFind润饰的函数,咱们都生成一个类,里边有一个办法能够打印咱们函数所在的文件方位(比方咱们需求检测某些函数有没有跨模块用的时分,其实就需求看这个函数的文件方位),生成kotlin文件的,一部咱们都是选用KotlinPoet去生成,进步咱们手动生成代码文件的容错性(不了解kotlinpoet的同学能够点这儿,运用手册)

object MyFuncHandler {
    var isInit = false
    @OptIn(KotlinPoetKspPreview::class)
    fun generate(
        codeGenerator: CodeGenerator,
        logger: KSPLogger,
        list: List<KSFunctionDeclaration>
    ) {
        if (isInit){
            return
        }
        isInit = true
        生成一个文件
        val file = FileSpec.builder("com.test.find.location", "MyFindLocation")
        val classBuilder = TypeSpec.classBuilder("FuncLocation")
        val fun2 = FunSpec.builder("myFunc")
        获取所有被注解润饰的函数的地址
        list.forEach {
            logger.warn("parent is " + it.parent.toString())
            val location = it.location
            if (location is FileLocation){
                留意这儿有点差异哦,咱们打印字符串用的是%S
                fun2.addStatement("Log.e(%S,%S)", "hello", location.filePath)
            }
        }
        classBuilder.addFunction(fun2.build())
        由于用到了Log,所以需求导入包,导包的写法如下
        file.addImport("android.util","Log")
        file.addType(classBuilder.build())
        codeGenerator写入文件,生成文件在build/ksp下
        file.build().writeTo(codeGenerator, false)
    }
}

值得留意的是,咱们process办法或许会履行多次,所以这儿加了一个符号位isInit,用于判断,防止生成多个文件导致冲突,终究生成文件如下:

image.png
生成的由于包含了个人信息,所以马赛克打上,生成的信息规则如下

生成的是全路径称号
/Users/xxx/包名/TestClass

总结

到这儿咱们应该对ksp有了一个开始的了解,只需apt能做到的,ksp相同也能做到,在kotlin占据主导的android开发国际中,生成kotlin文件的ksp,还不快学起来!!一起对于库开发者来说,迁移kapt到ksp也越来越主流了,信任我们都能把ksp用起来!