注解处理器与KSP:APT配置、KSP配置、增量处理、处理器选项、Room/Glide等实战

说到注解处理器,很多Android开发者的第一反应就是「慢」。确实,我早期做项目时,每次改个注解就要等几十秒重新编译,那种感觉真的很抓狂。后来KSP(Kotlin Symbol Processing)出来了,我才发现原来注解处理可以这么快。

这一章,我们就来聊聊APT和KSP的配置、增量处理,以及Room、Glide这些常用库的实战用法。

APT vs KSP:到底选哪个?

先说说APT(Annotation Processing Tool)。它是Java时代的产物,处理的是Java源码。Kotlin项目用APT,其实有个隐藏问题——Kotlin代码会被编译成Java存根,然后APT再处理这些存根。嗯,你想想看,多了一层转换,速度能快吗?

KSP就不一样了。它直接解析Kotlin源码,省掉了中间步骤。我在项目中实测过,同样的Room数据库代码,KSP比APT快了将近2倍。尤其是大型项目,这个差距会更明显。

核心结论: 新项目建议直接用KSP。老项目迁移也不难,大部分库都已经支持了。

APT配置:老派但稳定

如果你还在用APT,配置方式大概是这样的:

// 项目的 build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.kapt") version "1.9.0"
}

dependencies {
    // 声明处理器
    kapt("androidx.room:room-compiler:2.6.0")
    // 普通依赖
    implementation("androidx.room:room-runtime:2.6.0")
}

这里有个坑,我踩过好几次——kapt依赖必须写在dependencies块里,不能写在buildscript里。否则编译器会一脸懵,根本找不到处理器。

注意: kapt会生成Java存根,如果你的项目里Kotlin和Java混用,存根生成顺序可能会出问题。我曾经遇到过一个诡异bug,就是存根里少了一个方法,排查了半天才发现是kapt的锅。

KSP配置:又快又简单

KSP的配置就清爽多了。先加插件:

// 项目的 build.gradle.kts
plugins {
    id("com.google.devtools.ksp") version "1.9.0-1.0.13"
}

dependencies {
    // 直接用 ksp 替代 kapt
    ksp("androidx.room:room-compiler:2.6.0")
    implementation("androidx.room:room-runtime:2.6.0")
}

你看,就改了一个关键字。但背后的性能提升是实打实的。我个人习惯是,只要库支持KSP,我就优先用KSP。目前Room、Glide、Moshi这些主流库都已经支持了。

小技巧: 想知道某个库是否支持KSP?去它的GitHub页面看文档,一般都会标注「KSP support」。

增量处理:别再全量编译了

增量处理,说白了就是「只编译改动的部分」。默认情况下,注解处理器每次都会扫描所有文件。项目大了以后,每次改一行代码都要等半分钟,真的很崩溃。

开启增量处理其实很简单,在gradle.properties里加一行:

kapt.incremental.apt=true

对于KSP,它默认就是增量的,不需要额外配置。这也是KSP的一大优势。

不过要注意,增量处理不是万能的。如果你的处理器里写了全局状态(比如静态Map),增量处理可能会出问题。我遇到过的情况是,第一次编译正常,第二次编译就报「重复元素」错误。后来排查发现,是处理器里缓存了上一次的结果,没有清空。

避坑指南: 自定义处理器时,尽量不要用静态变量。如果非要用,记得在每次处理前清空。

处理器选项:给处理器传参数

有时候我们需要给注解处理器传一些参数,比如Room的数据库版本号。这就要用到处理器选项了。

在APT中,通过kapt { arguments { ... } }来配置:

kapt {
    arguments {
        arg("room.schemaLocation", "$projectDir/schemas")
        arg("room.incremental", "true")
    }
}

KSP的配置方式类似:

ksp {
    arg("room.schemaLocation", "$projectDir/schemas")
    arg("room.incremental", "true")
}

这些参数会被传递到处理器的init()方法里。你可以在自定义处理器中通过processingEnv.getOptions()获取。

实战经验: 我一般会把数据库版本号定义在gradle.properties里,然后通过处理器选项传给Room。这样改版本号时,只需要改一个地方。

Room实战:从APT迁移到KSP

Room是注解处理器的典型应用。我们来看看怎么从APT迁移到KSP。

第一步,替换插件和依赖:

// 替换 kapt 为 ksp
plugins {
    id("com.google.devtools.ksp") version "1.9.0-1.0.13"
}

dependencies {
    ksp("androidx.room:room-compiler:2.6.0")
    implementation("androidx.room:room-runtime:2.6.0")
    implementation("androidx.room:room-ktx:2.6.0")
}

第二步,配置处理器选项:

ksp {
    arg("room.schemaLocation", "$projectDir/schemas")
    arg("room.incremental", "true")
}

第三步,代码完全不用改。Room的注解(@Entity、@Dao、@Database)在KSP下完全兼容。

迁移完成后,你会发现编译速度快了很多。我自己的项目,从APT切到KSP后,增量编译时间从15秒降到了6秒。

Glide实战:注解处理器的另一种用法

Glide的注解处理器主要用于生成AppGlideModule的实现类。配置方式如下:

dependencies {
    // APT方式
    kapt("com.github.bumptech.glide:compiler:4.15.1")
    
    // KSP方式(Glide 4.15+ 支持)
    ksp("com.github.bumptech.glide:ksp:4.15.1")
    
    implementation("com.github.bumptech.glide:glide:4.15.1")
}

然后在代码里写一个AppGlideModule

@GlideModule
class MyAppGlideModule : AppGlideModule() {
    override fun applyOptions(context: Context, builder: GlideBuilder) {
        builder.setDefaultRequestOptions(
            RequestOptions().format(DecodeFormat.PREFER_RGB_565)
        )
    }
}

编译后,Glide会自动生成GlideApp类,你就可以用GlideApp.with(this).load(url).into(imageView)了。

个人建议: Glide的KSP支持还比较新,如果你的Glide版本较旧,建议先升级到4.15+再切KSP。

自定义注解处理器:从零开始

如果你需要自己写一个注解处理器,流程大概是这样的:

第一步,创建一个Java/Kotlin模块,专门放处理器代码。

第二步,实现AbstractProcessor类:

class MyProcessor : AbstractProcessor() {
    override fun getSupportedAnnotationTypes(): Set<String> {
        return setOf(MyAnnotation::class.java.canonicalName)
    }
    
    override fun process(
        annotations: Set<TypeElement>,
        roundEnv: RoundEnvironment
    ): Boolean {
        // 处理注解
        roundEnv.getElementsAnnotatedWith(MyAnnotation::class.java).forEach { element ->
            // 生成代码
        }
        return true
    }
}

第三步,注册处理器。在META-INF/services/javax.annotation.processing.Processor文件中写上处理器的全限定名。

嗯,这里要注意,KSP的自定义处理器写法不太一样,它用的是SymbolProcessor接口。不过核心逻辑是一样的——解析注解,生成代码。

SVG:注解处理核心流程

注解处理核心流程 Kotlin/Java 源码 包含 @Entity, @Dao 等注解 注解处理器 APT / KSP 解析注解 生成代码 RoomImpl, GlideApp 等 增量处理 处理器选项 APT 生成 Java 存根 → 慢 | KSP 直接解析 Kotlin → 快

常见问题与避坑

问题 原因 解决方案
编译报错「找不到符号」 处理器生成的代码没有被编译 检查依赖配置,确保处理器在annotationProcessor/ksp中
增量编译失效 处理器里用了静态变量或全局状态 避免静态变量,或在每次处理前清空
KSP和APT混用冲突 同一个库同时用了kapt和ksp 统一用一种方式,不要混用
Room数据库迁移失败 schema文件路径配置错误 检查room.schemaLocation参数是否正确

最后说一句,注解处理器这块,配置本身不难,难的是排查问题。我建议你在切换KSP时,先在一个小模块上试,确认没问题再全量迁移。这样即使出问题,影响范围也小。

好了,这一章的内容就到这里。记住,KSP是未来,APT是过去。能切就切,别犹豫。