Administrator
发布于 2026-09-22 / 6,131 阅读
0
0

KSP vs KAPT:Kotlin Symbol Processing 迁移实战

前言

如果你项目的全量编译要 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 中间层。直接在编译器层面拿到符号信息(类、函数、属性、注解),处理完输出新文件。

对比项

KAPT

KSP

工作方式

生成 Java 存根 + javac

直接读 Kotlin 语义树

增量编译

不支持

支持

语言支持

Kotlin/Java 混合

纯 Kotlin 为主

速度

基准 1x

约 2-3x

稳定性

成熟

1.0+ 已稳定

二、迁移步骤

第 1 步:确认依赖库是否支持 KSP

主流库的 KSP 支持情况:

库

KSP 支持

备注

Room 2.5+

是

room-compiler 有 KSP 版

Hilt 2.48+

是

hilt-compiler 有 KSP 版

Moshi

是

moshi-kotlin-codegen

Dagger 2.48+

是

Epoxy

部分

需要检查版本

Glide

否

仍在用 KAPT

如果你的项目只用了 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)的编译时间对比:

场景

KAPT

KSP

提升

全量编译

285s

112s

60%

增量编译(改一行)

45s

18s

60%

ABI 变更

180s

78s

56%

具体数字取决于项目规模和处理器数量,但"快一倍以上"是普遍结论。

五、写自己的 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 写桌面小组件。


评论