Moshi 的代码生成,KSP 适配后的编译速度

Moshi 的代码生成,KSP 适配后的编译速度

Moshi 的代码生成,KSP 适配后的编译速度


Moshi 的代码生成,KSP 适配后的编译速度


从 KAPT 的编译瓶颈说起


如果你在一个多模块的 Kotlin 项目里用了 Moshi 的 JsonClass 注解生成适配器,大概率被 kapt 的编译耗时折磨过。我印象很深的一次,是在把 Kotlin 版本升到 1.8.x 之后,某个包含三十几个模块的工程里,clean build 的时间直接突破了四分半钟。打开 Gradle Build Scan(gradle.com/build-scan,免费层级足够分析这类问题)一看,kaptGenerateStubskapt 两个任务链吃掉了接近百分之四十的构建时间。其中 Moshi Kotlin Codegen 所在的几个数据层模块,尽管每个模块只有不到五十个数据类,却稳稳占据了全链路的前几名。


KAPT 的瓶颈并不是 Moshi 本身的问题,而是架构层面的历史包袱。KAPT 为了让 Java 注解处理器能处理 Kotlin 代码,必须先生成 Java Stub。这意味着你的 Kotlin 源文件要先被解析一遍,转成一份给 Java APT 看的中间代码,Java 处理器在这份 Stub 上跑完之后,结果再映射回 Kotlin 编译链路。对于 Moshi 这种需要读取类结构、字段注解、泛型信息的代码生成器来说,Stub 的生成和往返映射完全是额外开销。模块越多,这份开销越线性增长,而且 KAPT 对增量编译的支持一直比较脆弱,经常一个数据类改了个字段名,就触发下游大量任务的重新编译。


Square 团队从 Moshi 1.13.0 开始正式在 moshi-kotlin-codegen 里引入了 KSP(Kotlin Symbol Processing)支持。到 1.15.0 这个阶段,KSP 路径已经成为我迁移项目的首选方案。它并不是简单的“换个注解处理器”,而是把 Moshi 的代码生成从 Java Stub 层直接拉进了 Kotlin 原生符号系统。


KSP 到底省在了哪里


KSP 由 Google 开发和维护,核心思路是直接操作 Kotlin 的 AST(抽象语法树),而不是通过 Java 注解处理器的间接层。Java APT 的 API 设计围绕 javax.lang.model 展开,这套 API 天生为 Java 服务,对 Kotlin 的顶层函数、扩展属性、密封类、值类等特性支持很差。KAPT 为了填补这个鸿沟,做了大量的适配和折中,代价就是编译时间和内存占用。


具体到 Moshi 的场景,KSP 省下的是 Stub Generation 和 Element 映射这两个大头。在 KAPT 下,当你在一个模块里声明了 @JsonClass(generateAdapter = true) data class User(...),KAPT 需要先调用 Kotlin 编译器的前端生成 Java 等效代码,让 Moshi 的处理器能在 Java 的 TypeMirror 体系里读到 User 的结构。这个过程中,Kotlin 特有的语法糖——比如数据类的 componentN 函数、默认参数、不可空类型的平台类型擦除——都需要在映射层被显式处理。KSP 则直接提供 KSClassDeclarationKSPropertyDeclaration 这类 Kotlin 原生的符号接口,Moshi 的 KSP 实现可以直截了当地问“这个类有哪些主构造函数参数”“这个字段有没有 @Transient 注解”,省去了中间层的二次翻译。


根据 Google KSP 团队在 GitHub 上的 benchmark 数据,以及社区里多个大型代码库的迁移反馈,KSP 在处理纯 Kotlin 项目时,通常能把注解处理阶段的时间缩短到 KAPT 的百分之五十到百分之七十五。对于 Moshi 这种生成工作量不算特别巨大的处理器,提升比例往往落在百分之三十到百分之五十之间,具体取决于模块数量和类的复杂度。这个数字听起来不算夸张,但放到每天数十次的增量编译循环里,节省的时间非常可观。


迁移步骤与版本地狱


把 Moshi 从 KAPT 切到 KSP,第一步不是改代码,而是对齐版本号。这是最容易踩坑的地方。KSP 的 Gradle 插件版本与 Kotlin 版本是严格绑定的,规则是 Kotlin版本号-1.0.x。比如你的项目用 Kotlin 1.9.22,那 KSP 插件就必须用 1.9.22-1.0.17。如果用了 Kotlin 1.9.10 却配了 1.9.22-1.0.17,Gradle 同步阶段就会抛出版本不兼容的错误,提示 ksp-1.9.22-1.0.17 is too new for kotlin-1.9.10。反过来也一样,KSP 版本低于 Kotlin 版本时,编译期会直接报错退出。


Gradle 配置层面,需要把模块的 build.gradle.kts 做三处改动。首先是插件块,去掉 kotlin("kapt"),换成 id("com.google.devtools.ksp") version "1.9.22-1.0.17"。其次是依赖配置,把 kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.0") 改成 ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")。最后,如果你之前为了 KAPT 开过 kapt { correctErrorTypes = true } 这类配置,可以直接删掉,KSP 不需要这种修正开关。


Moshi 的 KSP 支持是内建在同一个 moshi-kotlin-codegen artifact 里的,不需要换包名。Square 的做法是在 artifact 内部同时打包了 KAPT 和 KSP 两套处理器实现,Gradle 通过 kaptksp 配置决定加载哪一条路径。这一点比某些库要干净,比如 Room 在 KSP 支持早期需要换 artifact 或者额外的显式声明。


真正麻烦的是混编场景。很多 Android 项目不止用 Moshi,还用 Room、Dagger、或者 MapStruct。这些处理器如果还没迁移到 KSP,你的模块就得同时保留 kaptksp。Gradle 允许一个模块同时应用两个插件,但这意味着该模块的 Kotlin 代码会被两套系统各分析一遍:KSP 走原生符号处理,KAPT 继续走 Stub 生成。虽然功能上能跑,但编译时间并不能完全释放。我的建议是分模块迁移,优先把纯 Moshi 的数据模块切到 KSP,留给 Room 的模块暂时维持 KAPT,等业务层处理器跟上之后再整体切换。


实测:构建时间的真实变化


为了拿到可信的对比数据,我在一个包含 42 个模块的商用代码库里做了控制变量测试。这个代码库里有 11 个模块依赖 Moshi Codegen,总共生成大约 180 个 JsonAdapter。测试环境是 GitHub Actions 的 ubuntu-latest runner,Gradle 8.4,Kotlin 1

MockK 和 Mockito 的对比,Kotlin 项目怎么选 2026-08-26
Android 的 Foreground Service 类型限制,后台定位还能跑吗 2026-08-26

评论区