KSP初体验(一)

1,428 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情

KSP实践

  • 什么是KSP?
    它是Kapt的升级版,解析速度更快。

  • 那什么是Kapt了?
    kapt是apt的kotlin版,专门用来处理kotlin代码的。

  • 最后一问,那什么是apt?
    apt是java注解处理器,用于编译时期解析注解。很多第三方库都用到了注解处理器,例如Glide、Hilt、ARouter等,它们在编译时期去解析注解,然后根据注解内容去生成相关的文件。

  • 真真真的是最后一问了,为什么kapt比ksp慢?
    kapt的工作过程是先将kotlin翻译成java,再有apt去处理,而ksp可直接识别kotlin。

实现ARouter

不知道大家有没有使用过阿里的路由框架ARouter。ARouter是android实现组件化的路由框架,涉及到的功能有activity、fragment的跳转、跳转带参数等等,非常好用。
它其实也用到了编译时注解处理器,我们接下来动手撸一个乞丐版的ARouter——XRouter,相信写完后你会对ksp的理解更加深刻。我先把代码放在这里XRouter,实现过程中有什么不懂的地方自行查询。

ARouter原理

使用ARouter时,我们需要对Activity添加注解@Route(route = "path"),而ARouter会在代码编译时去搜集path信息,然后生成代码。 当用户调用ARouter.init时,它就会去使用反射调用自动生成的代码,将路由信息加载到内存。当用户再调用navigation时,它就会根据传入的path找到对应的路由信息,构建Intent实现跳转。

了解ARouter原理后我们就可以动手开始做了,就像把大象放进冰箱一样简单,这里划分为三步走。

第一步定义Annotation

新建模块,定义annotation:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Route(val route: String = "")

注意这里是将该annotation单独放到一个模块中。

第二步创建processor模块

添加依赖

第二步创建processor模块,这个模块是用来实现ksp功能,即注解处理器。

在模块的build.gradle.kts中加入以下代码:

plugins {
    kotlin("jvm")
}

group = "com.example"
version = "1.0-SNAPSHOT"


dependencies {
    implementation(kotlin("stdlib"))
    implementation("com.google.devtools.ksp:symbol-processing-api:1.5.31-1.0.0")
    implementation("com.squareup:kotlinpoet:1.10.2")
    implementation(project(":annotation"))
}

sourceSets.main {
    java.srcDirs("src/main/kotlin")
}

在项目根目录下的build.gradle.kts中加入以下代码:


buildscript {
repositories {
      google()
      mavenCentral()
  }
}

创建BuilderProcessorProvider


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

SymbolProcessorProvider默认实现create方法,需要return一个SymbolProcessor(注解处理器)。
注意到create方法的入参SymbolProcessorEnvironment,它提供了一些工具类:

  • CodeGenerator :管理生成文件
  • KSPLogger :用于编译时ksp的log

创建SymbolProcessor


class BuilderProcessor(
    val codeGenerator: CodeGenerator,
    val logger: KSPLogger
) : SymbolProcessor {

    var functionSpec : FunSpec.Builder? = null

    override fun process(resolver: Resolver): List<KSAnnotated> {
    
    }


    override fun finish() {
    
    }
}

这里注意process方法,入参resolver可以帮我们拿到所有编译时期的类、方法、属性等信息。再次提醒ksp是作用于编译时期。
理一下我们的思路,我们是需要拿到所有带@Route注解的类,然后再去使用kotlinpoet(不了解kotlinpoet的同学可以看我之前的文章)生成路由信息代码。

收集路由信息

在使用KotlinPoet之前,我们可以先把要生成的代码写下来,然后再来写KotlinPoet模板。

package com.example.xrouter

import android.app.Activity
import java.lang.Class
import java.util.HashMap
import kotlin.String
import kotlin.Unit

public class XRouterPathCollector {
  public fun loadInfo(map: HashMap<String, Class<out Activity>>): Unit {
    map["Main"] = com.holderzone.store.ksptest.MainActivity::class.java
    map["Second"] = com.holderzone.store.ksptest.SecondActivity::class.java
  }
}

主要实现loadInfo方法,传入hashmap,将路由path作为key,Class作为value。

接下来完善process方法。

override fun process(resolver: Resolver): List<KSAnnotated> {

        //获取所有带Route注解的类
        val symbols = resolver.getSymbolsWithAnnotation(Route::class.java.name)

        //使用kotlinpoet构建类型 HashMap<String, Class<out Activity>>
        val activity = ClassName("android.app", "Activity")
        val hashMap = ClassName("java.util", "HashMap")
        val classK = ClassName("java.lang", "Class")
        val stringK = ClassName("kotlin", "String")
        val classActivity = classK.parameterizedBy(WildcardTypeName.producerOf(activity))
        val hashMapSC = hashMap.parameterizedBy(stringK,classActivity)

        //使用kotlinpoet创建loadInfo方法
        functionSpec = FunSpec
            .builder("loadInfo")
            .addParameter(ParameterSpec.builder("map", hashMapSC).build())

        symbols
            .filter { it is KSClassDeclaration && it.validate() }
            .forEach {
                it as KSClassDeclaration
                val activityClass = ClassName(it.getPackageName(), it.simpleName.asString())
                //遍历该类所有的注解
                it.annotations.forEach {  
                    //找到Route注解的参数route
                    val resValue = it.arguments.find{ it.name!!.asString() == "route" }!!.value
                    val string1 = "map[\"${resValue}\"]"
                    val string2 = " = ${activityClass}::class.java"
                    //写入loadInfo方法中
                    functionSpec?.addStatement(string1+string2)
                }
            }
          
        return symbols.filter { !it.validate() }.toList()
    }


    override fun finish() {
        super.finish()
        //在ksp执行完后,创建文件XRouterPathCollector
        val funSpec = createFile("com.example.xrouter", functionSpec!!)
        writeFile(funSpec)
    }

    fun createFile(packageName:String, funSpec: FunSpec.Builder): FileSpec {
        //File
        val fileSpec = FileSpec.builder(packageName, "XRouterPathCollector")
        //Class
        val typeSpec = TypeSpec.classBuilder("XRouterPathCollector")
        typeSpec.addFunction(funSpec.build())
        fileSpec.addType(typeSpec.build())
        return fileSpec.build()
    }

    fun writeFile(fileSpec: FileSpec) {
        val file = codeGenerator.createNewFile(
            Dependencies.ALL_FILES,
            fileSpec.packageName,
            fileSpec.name
        )
        file.use {
            val content = fileSpec.toString().toByteArray()
            it.write(content)
        }
    }
    

至此,我们的功能就完成一大半了。