内存模型与 Swift 互操作
理解 Kotlin Multiplatform (KMP) 的内存模型,是编写高性能、无崩溃跨平台代码的关键。特别是对于 iOS 开发者,理解 Kotlin 的垃圾回收 (GC) 如何与 Swift 的自动引用计数 (ARC) 共存至关重要。
内存模型的演进:从“冻结”到“自由”
旧内存模型 (已废弃)
在 Kotlin 1.9 之前,Native 目标使用非常严格的内存规则:
- 对象隔离: 跨线程共享的对象必须是“冻结”的(
freeze())。 - 不可变性: 冻结后的对象变为不可变,任何修改都会抛出
InvalidMutabilityException。 - 痛点: 开发者需要花费大量时间处理对象冻结,代码极其臃肿。
新内存模型 (默认开启)
从 Kotlin 1.9.20 开始(并在 Kotlin 2.3.0 中进一步优化),所有平台统一采用现代内存管理器:
- 完全线程共享: 就像在 JVM 上一样,您可以在不同线程间自由读写对象。
- 后台 GC: Kotlin/Native 拥有独立的垃圾回收器,能在后台清理不再使用的对象。
- 无缝衔接: 开发者不再需要手动调用
freeze()。
Kotlin GC vs Swift ARC
当 Kotlin 暴露给 Swift 时
,会发生以下奇妙的化学反应:
- Swift 视角: Kotlin 对象看起来就像一个普通的 Swift 类实例,遵循 ARC 规则。
- Kotlin 视角: 只要 Swift 侧还持有一个 Kotlin 对象的引用,该对象在 Kotlin 堆中就不会被回收。
- 循环引用: ⚠️ 这是唯一的风险点。如果一个 Kotlin 对象持有一个 Swift 对象,而该 Swift 对象又持有回这个 Kotlin 对象,GC 与 ARC 的交叉引用可能导致内存泄漏。
解决方案
在 Swift 侧使用 weak 引用,或者在 Kotlin 侧使用 WeakReference 来打破循环。
调用协程:Suspend 函数转换
Swift 侧调用 Kotlin
的 suspend 函数时,Kotlin 编译器会自动将其转换为带回调的形式,或(在最新版中)支持 Swift Async/Await。
kotlin
class UserRepository {
suspend fun getUserName(): String {
delay(1000)
return "Virogu"
}
}swift
// 需要使用 SKIE 插件或最新编译器特性
let repo = UserRepository()
Task {
do {
let name = try await repo.getUserName()
print("User: \(name)")
} catch {
print("Error: \(error)")
}
}性能调优:减少跨界开销
跨平台调用
(Crossing the boundary)虽然方便,但并非完全免费:
- 数据转换: 传递基础类型(Int, Double)极快。传递复杂的
List或Map会触发底层的类型转换和复制。 - 调用频率: 避免在每一帧(如绘图回调)中频繁跨界调用。建议将大块逻辑放在 Kotlin 侧处理,一次性返回结果。
- 大对象处理: 如果要传递巨型 JSON 或图片字节流,考虑直接传递原始指针(
ByteArray的 C 指针),以避免不必要的内存拷贝。