携手创作,共同成长!这是我参与「掘金日新计划 · 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)
}
}
至此,我们的功能就完成一大半了。