Swift 互操作:从"能跑"到"好用"
9 月 7 日,JetBrains 发布了 Kotlin 2.4.20。
如果你只是扫一眼更新日志,可能会觉得"哦,又是个修修补补的版本"——毕竟两百多条变更里,大半是 bugfix。
但对写 KMP 的人来说,这个版本有几个东西是真香的。尤其是 Swift 互操作这块,终于补上了长期以来的几块短板。
先说结论:这次最值得关注的是三个方向——Swift 互操作能力、编译速度、跨平台一致性。下面一个个拆。
一、Swift export 支持 sealed class:iOS 同学的 switch 终于不用写 default 了
用 KMP 写过 iOS 的人,大概率都遇到过这个别扭的场景。
你在 Kotlin 里定义了一个很优雅的 sealed interface:
sealed interface Shape
class Circle : Shape
class Rectangle : Shape
fun createCircle(): Shape = Circle()
逻辑上天衣无缝,Kotlin 侧 pattern matching 一把梭。
但到了 Swift 那边,Kotlin 的 sealed 类型只能当成一个普通类来用,你根本没法做穷尽匹配,只能写一堆 if 判断,最后再兜一个 default。
更难受的是:哪天 Kotlin 侧加了新的子类,Swift 侧的编译不会报错——于是线上就可能出现一个没人处理的 case。
2.4.20 把这个问题解决了。sealed 类型现在会映射成 Swift 的 enum,编译器会生成一个 sealedType() 方法:
let shape = createCircle()
let name = switch shape.sealedType() {
case let .circle(type): "It's a \(type.value)"
case let .rectangle(type): "It's a \(type.value)"
}
注意这里——没有 default。
Swift 编译器会检查穷尽性,而且因为映射成了 enum,Xcode 的自动补全也跟着好用了。哪天 Kotlin 侧加了新的 sealed 子类,Swift 侧会直接编译报错提醒你处理。
这比"运行时才发现漏了 case"强太多了。
二、跨语言继承:用纯 Swift 库,不用再绕路了
第二个变化更实际:Swift export 支持跨语言继承了。
什么意思?说个真实场景——你想在 iOS 侧用 CryptoKit 这个纯 Swift 库,但它没法直接导入 Kotlin。
以前的做法要么是写一层 Objective-C 桥接,要么干脆放弃,在 Kotlin 侧找个替代实现。现在可以直接这么干:
Kotlin 侧声明接口和一个开放基类:
interface CryptoProvider {
fun hashMD5(input: String): String
}
fun processHash(provider: CryptoProvider, input: String): String =
provider.hashMD5(input)
open class SwiftBase
Swift 侧继承基类、实现接口,然后把对象传回 Kotlin:
import CryptoKit
final class IosCryptoProvider: SwiftBase, CryptoProvider {
func hashMD5(input: String) -> String {
guard let data = input.data(using: .utf8) else { return "failed" }
return Insecure.MD5.hash(data: data).description
}
}
let provider = IosCryptoProvider()
print(processHash(provider: provider, input: "Hello, world!"))
Kotlin 收到这个 Swift 对象时,就把它当成接口的普通实现来调用。
这就是官方说的 reverse import 模式:契约写在 Kotlin,实现在 Swift。对那些必须依赖平台原生库的场景,这条路终于顺了。
三、增量编译进 Beta:改一行代码,不用再等一杯咖啡
这条可能是最能改善日常开发体验的。
Kotlin/Native 的编译速度一直是 KMP 的痛点之一。klib 的增量编译其实从 1.9.20 就有了,但一直在打磨。这次正式进入 Beta,官方说 debug 构建的编译时间大幅减少。
打开方式很简单,在 gradle.properties 里加一行:
kotlin.incremental.native=true
官方还提到,他们计划在后续版本里默认开启这个选项。
顺带一提,导出 XCFramework 的时候,assembleSharedXCFramework 任务现在会自动生成 Package.swift 文件。以前发布带 SwiftPM 依赖的 XCFramework,得自己处理 SwiftPM 包的分发问题,现在省事了。
四、Wasm 侧:几个不容易注意但很关键的改动
如果你的项目有 web 端,这几点值得留意。
伴生对象初始化顺序对齐 JVM
以前 Kotlin/Wasm 里,父类和子类的 companion object 初始化顺序可能和 JVM 相反。跨平台项目里,这种差异最容易变成"本地跑得好好的,换个平台就出鬼"的 bug。现在统一按 JVM 的行为来了。
Wasmtime 运行时支持
wasmWasi 目标以前只能用 Node.js 跑,还得先做一层 JavaScript 引导。现在可以直接用 Wasmtime 这个独立的 WebAssembly 运行时:
kotlin {
wasmWasi {
wasmtime()
}
}
新增编译模式
以前只有 monolith 模式(整个项目和依赖一起编译,生成单个二进制,体积最小但每次都全量)。现在多了两个多模块模式,可以按需选择:
- 开发时用
multimodule-closed-world,只重编译改动的模块,迭代更快 - 生产构建仍用 monolith,保证产物最小
配置方式:
kotlin.wasm.compilationMode=multimodule-open-world
注意一个 breaking change:@JsFun 声明里现在用顶层 require() 会直接报错。如果之前这么写过,得迁移到 @JsModule 注解或者 import() 表达式。
五、Kotlin/JS:Karma 退役,Playwright 接棒
Kotlin/JS 的浏览器测试一直用 Karma 当启动器,但 Karma 项目已经停止维护两年了。
2.4.20 引入了新的测试 DSL:底层换成 Playwright 驱动(支持 Chromium、Firefox、WebKit 三家),测试运行器用 Mocha,打包工具暂时还是 webpack(官方说后续会换 Vite)。
用法是在 browser {} 里加一个 test {} 块:
kotlin {
js {
browser {
@OptIn(ExperimentalJsTestDsl::class)
test {
timeout = 2.seconds
chromium()
firefox()
webkit()
}
}
}
}
另外,suspend lambda 现在可以导出成 JavaScript 的 async 函数了——这个对跨端调用挺实用。
六、标准库和其他
几个顺手能用的:
- 协程堆栈恢复:新增
StackTraceRecoverable接口。协程之间抛异常时,能看清异常从哪来、被谁重新抛出。自带复杂构造参数的异常,实现一下copyForStackTraceRecovery()就行。 - 集合工具:新增了检查集合元素相等性和唯一性的函数。
- kotlin.test:新增断言函数重载。
- Gradle:完全兼容 Gradle 7.6.3 到 9.7.0;Problems API 现在带编译器诊断 ID 并做了分组,排错时能快速定位问题来源。
- 编译器:JVM 21+ 上
when表达式的 invokedynamic 编译从实验转正(默认开启);新增kotlinr运行命令和独立的 native image。
这些变化对 KMP 开发者意味着什么
把上面串起来看,我觉得 2.4.20 释放了三个信号:
1. iOS 互操作正在从"能用"走向"好用"
sealed class 映射成 enum、跨语言继承支持纯 Swift 库——这两条直击 KMP 在 iOS 侧最常被吐槽的点。以前大家说 KMP “Android 体验好,iOS 差点意思”,现在这个差距在肉眼可见地缩小。
2. 编译体验被当成正经事在解决
klib 增量编译进 Beta、Wasm 多模块编译模式,都是在解决同一件事:让日常开发不用等。编译速度是 KMP 最现实的劝退点之一,这方向对了。
3. 跨平台一致性越来越被重视
Wasm 的伴生对象初始化顺序对齐 JVM,看起来是个小改动,但这类"平台间行为不一致"恰恰是跨平台项目最难缠的问题。官方在主动收窄这些差异。
顺带提一句另一条新闻:Google 前几天发布了 ADK for Kotlin 1.0,可以用 Kotlin 写生产级 AI Agent。Kotlin 和 AI Agent 这两个圈子,今年的交集肉眼可见地多起来了。
升级建议
- 一般项目:直接升。这个版本以修复为主,风险不大。
- KMP 项目:强烈建议升,Swift export 那几个改动是实打实的体验提升。
- 用 Wasm 的项目:升级前先检查
@JsFun里的require()调用,这是唯一的 breaking change。 - 想试增量编译:先加
kotlin.incremental.native=true试试水,有问题及时反馈给官方。
Kotlin 的版本节奏一直很稳,不搞大跃进。这种"每次解决几个真问题"的迭代方式,其实对生产项目最友好。
