Kotlin 2.0 正式发布,迁移要注意什么
「Kotlin 2.0 正式发布,迁移要注意什么」
Kotlin 2.0 在 2024 年 5 月 GA 了,JetBrains 憋了快两年的大版本终于落地。我先把结论放在前面:这个版本最值得关注的不是新语法糖,而是编译器后端彻底换成了 K2,以及随之而来的一堆迁移陷阱。如果你现在手里的项目还在用 1.9.x,别急着点那个升级按钮,先看看你的构建链能不能扛住。
K2 编译器:换引擎不是换皮肤
Kotlin 2.0 的核心卖点是 K2 编译器正式取代旧版前端。JetBrains 从 2021 年就开始吹这个架构重写,说是要解决旧编译器里层层叠叠的技术债。旧前端的问题确实够呛:类型推断在复杂泛型场景下经常抽风,编译速度随着代码量指数级恶化,IDE 高亮和实际编译结果不一致的 bug 能凑一个独立项目。
K2 的新架构把词法分析、语义分析、中间表示这几个阶段彻底拆开了,用 FIR(Frontend Intermediate Representation)作为统一中间层。理论上这能让编译速度提升 30% 到 50%,IDE 响应更快,而且为后续的语言特性扩展打下基础。JetBrains 在官方博客里的原话是 "a solid foundation for the next decade of Kotlin evolution",这话听着耳熟,基本上每个搞重构的 team 都会这么说。
但问题就出在这个 "foundation" 上。K2 的 FIR 层和旧编译器的 AST 表示完全不兼容,这意味着所有依赖编译器插件的工具都得重写适配。这不是 JetBrains 自己改改就能完事的,整个生态要跟着动。
我实际测试了一个中等规模的 Android 项目,大概 15 万行 Kotlin 代码,用了 KSP、Room、Hilt、Compose Compiler 这几个主流插件。升级过程如下:先把 Kotlin 版本改到 2.0.0,Gradle sync 直接报错,提示 Compose Compiler 1.5.x 不兼容 K2。查了一下,Compose Compiler 的 K2 支持是在 2.0.0 版本才提供的,而 Android Gradle Plugin 8.2 默认带的还是 1.5.x。这意味着你要先升级 AGP 到 8.3 以上,或者手动指定 Compose Compiler 版本。
升完 AGP 8.3,KSP 又炸了。KSP 2 的预览版在 Kotlin 2.0 发布前几个月才出来,很多 processor 还没适配。Room 的 KSP 实现倒是更新得快,但 Dagger/Hilt 那边就慢了半拍。我用的 Hilt 2.51 版本,编译时直接抛 IllegalStateException: KSP 2 is not supported yet,堆栈信息指向 Hilt 的代码生成逻辑还在调旧版 KSP API。最后被迫回退到 KSP 1,用 ksp.useKSP2=false 这个 flag 强行压制,才算把项目编过去。
这种插件版本的矩阵依赖,是迁移 Kotlin 2.0 时最折磨人的地方。JetBrains 在发布说明里列了一张 "兼容性表格",看着挺全,但实际操作中你会发现很多边缘工具没在上面。比如我们项目里用的 kotlinx-serialization 插件,1.6.x 版本在 K2 下会生成错误的序列化代码,升级到 1.7.0-RC 才解决。再比如 Detekt 静态分析工具,1.23.x 版本根本不支持 K2,必须等 2.0 重构版,而那个版本到现在还是 beta。
语言特性:稳定了,但也没那么激动人心
K2 编译器落地的同时,JetBrains 把几个长期 preview 的特性正式稳定化了。最显眼的是 data object,这是 data class 的单例版本,语法上就是把 object 前面加个 data 修饰。
data object NetworkError : Error()这玩意儿的实际价值在于自动生成 toString()、equals()、hashCode(),让单例对象在日志和测试断言里更友好。以前写 sealed class 的状态机,叶子节点用 object 的话,toString() 输出是带包名的全限定类名,测试失败时看日志头疼。现在 data object 会输出简洁的 NetworkError,确实省了点事。
但这个特性从 Kotlin 1.7.20 就开始 preview,磨了快两年才稳定,社区早就用脚投票了。很多项目要么自己写了个小注解处理器来生成这些方法,要么干脆忍受丑陋的默认输出。JetBrains 这个节奏把控,说好听叫谨慎,说难听就是优先级混乱——K2 编译器这种底层重构占了太多资源,语言特性只能排队。
另一个稳定化的特性是 sealed 的扩展使用场景,允许 sealed class 和它的子类定义在不同的源文件里,只要同属于一个模块。这打破了之前 "必须同文件" 的限制,对大型项目的分层架构有点用。但说实话,这个限制本来就是 Kotlin 设计里比较奇怪的地方,Java 的 sealed class 从第一天就允许跨文件,Kotlin 算是补了个迟到三年的课。
context receivers 这个特性倒是让人期待,但 Kotlin 2.0 里它还在实验阶段,而且 JetBrains 放风说可能要改语法设计。这个特性解决的是依赖注入和作用域管理的表达力问题,用 context(T) 的语法把隐式参数显式化。比如:
context(Logger, CoroutineScope)
fun processData() {
// 直接调用 logger.info() 和 launch {},不需要显式接收参数
}这看起来像是 effect system 的简化版,但实际编译器支持非常粗糙,IDE 补全经常漏掉 context 里的成员,错误提示也云里雾里。我尝试在一个小模块里引入,结果团队成员看了代码直摇头,说 "这比直接传参数还难懂"。JetBrains 的文档自己也承认 "the design is not final",建议不要在生产代码用。那这特性放出来干嘛?大概是给社区画饼,对冲 K2 迁移的痛苦。
Gradle 和构建链:版本地狱变本加厉
Kotlin 2.0 对 Gradle 版本的要求是 8.5 以上,而很多 Android 项目还在 Gradle 7.x 甚至 6.x 的泥潭里挣扎。Gradle 8 的升级本身就不是无痛的——配置缓存、非传递性依赖、构建脚本语法变化,每一样都能消耗你几个下午。
更隐蔽的问题是 Kotlin Gradle Plugin 的 API 变化。2.0 版本里 kotlinOptions 这个 DSL 块被标记为废弃,推荐改用 compilerOptions。但 compilerOptions 的类型是 KotlinCommonCompilerOptions,和旧的 KotlinJvmOptions 不完全兼容。比如以前设置 JVM target 的写法:
kotlinOptions {
jvmTarget = "11"
}现在要改成:
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}看起来只是语法糖,但 compilerOptions 是在 KotlinCompile task 的配置阶段才生效的,而某些自定义构建逻辑可能在 task 注册阶段就要读取这些值。我们项目里有个自定义插件,用来根据 JVM target 选择不同的 native 库版本,升级后因为生命周期错位,总是读到默认值 JVM_1_8,导致 ABI 不兼容的 crash。最后是靠在 afterEvaluate 里延迟初始化才解决,这种 workaround 写起来恶心,维护起来更恶心。
Kotlin 2.0 还引入了一个新的 Gradle DSL:kotlin("jvm") version "2.0.0" 这种插件应用方式被推荐替代旧的 apply plugin。但 kotlin-dsl 插件和这种新语法有冲突,在某些脚本里会导致 PluginApplicationException,根因是 Gradle 的插件解析顺序变了。JetBrains 的 issue tracker 里 KT-68278 这个 ticket 到现在还是 open 状态,workaround 是混用两种语法,丑得要命。
Android 生态的特殊麻烦
Android 开发者迁移 Kotlin 2.0 的阻力,比纯后端或 KMP 项目大得多。核心矛盾在于 Google 的 toolchain 和 JetBrains 的 release 节奏不完全同步。
Compose Compiler 是最敏感的环节。这个插件直接钩进 Kotlin 编译器的 IR(Intermediate Representation)阶段,把 @Composable 函数转换成支持重组的状态机代码。K2 的 FIR 层到 IR 的映射和旧编译器不同,所以 Compose Compiler 必须重写适配。Google 的 Compose Compiler 2.0.0 版本确实在 Kotlin 2.0 GA 前后发布了,但问题是它的版本号和 Kotlin 版本号强绑定——Compose Compiler 2.0.0 对应 Kotlin 2.0.0,2.0.10 对应 Kotlin 2.0.10,这种耦合让升级变成全有或全无的赌博。
更麻烦的是 Compose Compiler 的发布往往滞后 Kotlin 小版本一到两周。如果你的项目依赖了某个 Kotlin 2.0.x 的 bugfix,但 Compose Compiler 还没出对应版本,你就卡在中间上不去下不来。我们团队遇到过这种情况:Kotlin 2.0.10 修复了一个影响 inline 函数内联的 crash,但 Compose Compiler 2.0.10 延迟了五天发布。那五天里,要么忍受 crash,要么回退 Kotlin 版本放弃 bugfix。
AGP 和 Kotlin 的兼容性矩阵也是头疼事。AGP 8.2 官方支持 Kotlin 1.9.x,8.3 开始支持 2.0,但 8.3 本身又要求 Gradle 8.4 以上,而且改掉了 buildConfig 的默认生成行为。我们项目里有几十个模块依赖 BuildConfig 字段来做 feature toggle,升级后全部编译失败,得手动在每个模块的 build.gradle 里加 buildConfig = true。这种 "默认行为翻转" 的升级策略,Google 这几年玩得越来越溜,每次大版本都能制造一批机械劳动。
R8 和 ProGuard 的规则在 Kotlin 2.0 下也有变化。K2 编译器生成的字节码结构和旧版略有不同,某些反射访问的类名、方法名在 obfuscation 后映射关系变了。我们上线后通过 Firebase Crashlytics 捕获到一个 NoSuchMethodException,追查发现是 Moshi 的 generated adapter 在找 Kotlin 生成的 copy$default 方法,但 R8 把方法名洗成了不同的 obfuscated name。旧版 Kotlin 和 R8 的组合没这问题,升级到 2.0 后触发了。最后是在 proguard-rules 里加了一行 -keepclassmembers class __PLACEHOLDER_ITALIC_0____PLACEHOLDER_ITALIC_1__(...); } 解决,但这种问题在测试阶段完全没暴露,因为 debug 构建不开 R8。
Kotlin Multiplatform:2.0 的最大赢家,也是最危险的赌注
如果说 Kotlin 2.0 对哪个场景最友好,那无疑是 KMP(Kotlin Multiplatform)。K2 编译器的架构重构很大程度上是为 KMP 服务的——统一的 FIR 层让 JVM、Native、JS/WASM 三个后端的代码共享更容易,编译速度提升在 KMP 项目里感知最明显。
JetBrains 在 2024 年把 KMP 的 status 从 "experimental" 改成了 "stable",配合 Kotlin 2.0 发布做了一波营销。实际体验上,KMP 的 Gradle 配置确实比 1.9 时代简洁了不少,kotlin("multiplatform") 插件的 DSL 更统一,source set 的层级关系也更清晰。
但 KMP 的 "stable" 是有水分的。iOS 端的内存管理模型从 legacy 切换到 new memory manager 才没多久,虽然 2.0 默认用新的,但很多 C 互操作场景还有 leak 的坑。我们有个 KMP 模块封装了 SQLite 的 native driver,在 iOS 上跑压力测试时,native heap 持续增长直到被系统 kill。用 Xcode 的 Instruments 追查,发现 Kotlin/Native 的 StableRef 在回调闭包里没正确释放,而同样的代码在 Android JVM 端完全正常。JetBrains 的 YouTrack 上 KT-64532 这个 issue 描述了类似问题,状态是 "In Progress" 已经四个月。
KMP 的第三方库生态也是痛点。Kotlin 2.0 要求库重新发布 K2-compatible 版本,但很多 KMP 库的维护者是个人开发者,升级动力不足。比如 kotlinx-datetime 这种 JetBrains 亲儿子倒是跟进了,但 Ktor 的某些 client engine 在 2.0 下有 breaking change,OkHttp 引擎的 TLS 配置行为变了,导致我们的 HTTP 请求在 Android 12 以下设备握手失败。追查发现是 Ktor 3.0.0-beta 里为了统一多平台行为,把证书校验逻辑改得更严格,但文档里只字未提。
Compose Multiplatform 作为 KMP 的 UI 层,在 Kotlin 2.0 下的状态尤其微妙。JetBrains 把 Compose for Desktop 和 Compose for iOS 都并进了 Compose Multiplatform 的 umbrella,但 iOS 端的性能还是个硬伤。我在 M2 Mac 的 iOS simulator 上跑了个中等复杂度的列表页面,帧率能掉到 40fps 以下,同样的代码在 Android emulator 上稳 60fps。用 Instruments 看,瓶颈在 Kotlin/Native 到 Objective-C 的 bridge 调用,每个重组周期都有大量的 objc_msgSend。这个问题不是 Compose 特有的,但 UI 框架的高频调用放大了它。
迁移策略:我的实际建议
说了这么多坑,那到底要不要升?我的判断是分场景。
如果你的项目是纯 Android 应用,没有 KMP,没有复杂的编译器插件,且团队有精力处理构建链升级,那可以升。收益主要是编译速度提升和 IDE 体验改善,K2 的增量编译确实比旧版稳,改一行代码重新编译的时间从秒级降到亚秒级。但要注意控制升级节奏:先在一个 feature module 里试点,不要全量切。我们团队的实践是先升级了 data layer 的纯 Kotlin 模块,UI 层留在 1.9.x 等 Compose Compiler 稳定,两周后再切 UI 层。
如果你的项目重度依赖 KSP 代码生成,比如 Room、Moshi、Dagger 全家桶,建议等 KSP 2 正式版和各 processor 适配完成。现在(2024 年中)的状态是能用但别扭,flag 和 workaround 太多,CI 构建的稳定性会受影响。
如果你有 KMP 模块,尤其是 share 了 iOS 端,Kotlin 2.0 几乎是必升的,因为 K2 对 Native 后端的改进确实能解决一些旧编译器的顽疾。但要预留足够的测试时间,特别是内存和压力测试。我们 iOS 端的上线前测试周期因为 KMP 的 native leak 问题延长了两周,这在排期时完全没预料到。
一个具体的版本组合参考:我们最终跑通的配置是 Kotlin 2.0.10、AGP 8.4.1、Gradle 8.7、Compose Compiler 2.0.10、KSP 1.0.1(非 KSP 2)、Hilt 2.51.1。这个组合不是最新,但是经过互相验证的稳定交集。每次想升级其中任何一个,都要先查其他几个的兼容性矩阵,这种版本耦合是 Android 生态的老毛病了,Kotlin 2.0 没治好,反而因为 K2 的 breaking change 更严重。
对 JetBrains 的一点吐槽
Kotlin 2.0 的发布策略,我觉得 JetBrains 有点被自己的技术野心绑架了。K2 编译器的重构是必要的,但两年的过渡期里,语言和生态的演进几乎停滞。对比之下,Java 21 的 virtual threads、pattern matching、sequenced collections 这些特性一个接一个,Google 也在推 Android 专用的 Java 改进(虽然主要是 ART 层面的)。Kotlin 的 "better Java" 叙事,在语言特性层面已经很久没拿出让人兴奋的东西了。
JetBrains 的资源分配也令人疑惑。K2 编译器、KMP、Compose Multiplatform、Fleet IDE、Amper 构建工具,这几条线同时推进,每条都需要生态适配。结果是每条线都半熟不熟,开发者被迫当付费测试员。Amper 这个新项目尤其迷——JetBrains 想用它替代 Gradle,但 Kotlin 2.0 的发布说明里 Amper 还是 "prototype",和 K2 的 migration guide 完全不提它。那你推这玩意干嘛?
社区治理方面,Kotlin 的公开决策过程比 Java 的 JEP 流程 opaque 得多。很多 breaking change 的动机只在 JetBrains 内部论坛或 KotlinConf 的闭门演讲里透露,普通开发者只能看 release note 的寥寥数语猜意图。比如 context receivers 的反复延期,官方解释是 "收集反馈",但具体什么反馈、可能怎么改,完全没有公开渠道跟踪。这种黑箱操作和 Kotlin 早期社区驱动的形象越来越远了。
结尾
Kotlin 2.0 是个重要的技术节点,但 "重要" 不等于 "必须立刻升级"。K2 编译器的长期价值是真实的,只是这个价值现在被迁移成本稀释了。JetBrains 赌的是两年后生态完成适配,开发者享受编译速度和语言扩展性的红利。但这个赌注能不能兑现,取决于 Google、Gradle、Square 这些生态玩家的配合度,而这些都不是 JetBrains 能控制的。
我现在的心态是:Kotlin 2.0 已经进了我们项目的 main branch,但 feature flag 还留着,随时可以回退。下一个大版本 Kotlin 2.1 据说要引入更好的 effect system 和 memory model 改进,到时候可能又是一轮适配。这种永无止境的升级 treadmill,到底是技术演进还是 vendor lock-in 的变体,我越来越分不清了。
你们项目升 2.0 了吗?KSP 2 敢不敢开?