前言
如果你项目的全量编译要 5 分钟以上,其中可能有 1-2 分钟花在 KAPT 上。KAPT 生成 Kotlin 存根(stub)再交给 Java 注解处理器跑,等于每次编译都多做一遍无用功。
KSP(Kotlin Symbol Processing)是 JetBrains 官方做的替代方案,直接读 Kotlin 的语义树,不生成存根,不经过 javac。实测同一个处理器,KSP 比 KAPT 快 2-3 倍。
一、为什么 KAPT 慢
KAPT 的工作流程:
Kotlin 源码 -> [KAPT 生成 Java 存根] -> [javac 编译存根] -> [注解处理器执行] -> 生成代码
它必须把 Kotlin 代码"翻译"成 Java 存根,因为传统注解处理器(如 Dagger 的、ButterKnife 的)只认 Java 语法。这个翻译过程是全量的,改一行代码也要重新生成所有存根。
KSP 的工作流程:
Kotlin 源码 -> [KSP 直接读语义树] -> 生成代码
没有存根、没有 javac 中间层。直接在编译器层面拿到符号信息(类、函数、属性、注解),处理完输出新文件。
二、迁移步骤
第 1 步:确认依赖库是否支持 KSP
主流库的 KSP 支持情况:
如果你的项目只用了 Room 和 Hilt,迁移很顺利。如果还依赖不支持 KSP 的库,可以 KAPT + KSP 共存。
第 2 步:加 KSP 插件
// settings.gradle.kts
plugins {
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}
// app/build.gradle.kts
plugins {
id("com.google.devtools.ksp")
id("dagger.hilt.android.plugin")
id("com.android.application")
kotlin("android")
}
第 3 步:替换 KAPT 为 KSP
// Before
dependencies {
kapt("androidx.room:room-compiler:2.6.1")
kapt("com.google.dagger:hilt-compiler:2.48")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")
}
// After
dependencies {
ksp("androidx.room:room-compiler:2.6.1")
ksp("com.google.dagger:hilt-compiler:2.48")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")
}
第 4 步:移除 KAPT 插件
// 删掉这行
// id("kotlin-kapt")
// 删掉所有 kapt(...) 依赖
第 5 步:处理 KAPT 特有配置
有些库在 KAPT 下有额外配置项,迁移后语法不同:
// KAPT 方式
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
}
}
// KSP 方式
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
三、混合共存方案
如果项目里有不支持 KSP 的库(比如 Glide),可以同时用:
plugins {
id("kotlin-kapt") // 给 Glide 用
id("com.google.devtools.ksp") // 给 Room/Hilt 用
}
dependencies {
ksp("androidx.room:room-compiler:2.6.1")
ksp("com.google.dagger:hilt-compiler:2.48")
kapt("com.github.bumptech.glide:compiler:4.16.0") // Glide 仍用 KAPT
}
共存不影响功能,只是 Glide 部分仍然慢。等 Glide 迁移到 KSP 后再移除 KAPT。
四、实测对比
一个中型项目(200+ 模块、Room + Hilt + Moshi)的编译时间对比:
具体数字取决于项目规模和处理器数量,但"快一倍以上"是普遍结论。
五、写自己的 KSP 处理器
如果你有自定义注解处理需求,KSP 的 API 比 KAPT 干净很多。
定义注解:
@Target(AnnotationTarget.CLASS)
annotation Router(val path: String)
写处理器:
class RouterProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
// 找所有被 @Router 标注的类
val symbols = resolver.getSymbolsWithAnnotation(Router::class.qualifiedName!!)
val deferred = mutableListOf<KSAnnotated>()
val routes = mutableListOf<RouteInfo>()
symbols.forEach { symbol ->
val ksClass = symbol as? KSClassDeclaration
if (ksClass == null) {
deferred.add(symbol)
return@forEach
}
val path = ksClass.annotations
.first { it.annotationType.resolve().qualifiedName?.asString() == Router::class.qualifiedName }
.arguments.first().value as String
routes.add(RouteInfo(path, ksClass.qualifiedName!!.asString()))
}
if (routes.isNotEmpty()) {
// 生成 RouterTable.kt
val fileContent = buildString {
appendLine("package com.example.generated")
appendLine()
appendLine("object RouterTable {")
appendLine(" val routes = mapOf(")
routes.forEach { route ->
appendLine(" \"${route.path}\" to ${route.className}::class,")
}
appendLine(" )")
appendLine("}")
}
codeGenerator.createNewFile(
Dependencies(true),
"com.example.generated",
"RouterTable"
).bufferedWriter().use { it.write(fileContent) }
}
return deferred
}
data class RouteInfo(val path: String, val className: String)
}
注册处理器:
class RouterProcessorProvider : SymbolProcessorProvider {
override fun processor(
environment: SymbolProcessorEnvironment
): SymbolProcessor = RouterProcessor(environment.codeGenerator, environment.logger)
}
在 META-INF/services/ 下放文件:
// 文件名: com.google.devtools.ksp.processing.SymbolProcessorProvider
// 内容: com.example.RouterProcessorProvider
小结
KSP 迁移的核心决策点:你的依赖库是否都支持 KSP。支持就全量迁移,不支持就共存。
收益很直接:编译时间砍一半以上,增量编译从"改一行等 40 秒"变成"改一行等 15 秒"。
下篇讲 App Widget 的新架构 Glance,用 Compose 写桌面小组件。