6.3.5.2 与 Swift/Objective-C ARC 的集成
原文链接: https://kotlinlang.org/docs/native-arc-integration.html
6.3.5.2 与 Swift/Objective-C ARC 的集成
Kotlin 与 Objective-C 使用不同的内存管理策略。Kotlin 使用追踪式垃圾回收器,而 Objective-C 依赖自动引用计数(ARC)。
这两种策略之间的集成通常是无缝的,一般不需要额外的工作。不过,有一些细节需要你留意:
线程
析构器
如果 Swift/Objective-C 对象是在主线程上传给 Kotlin 的,那么这些对象及其所引用对象的析构会在主线程上调用,例如:
1
2
3
4
5
6
| // Kotlin
class KotlinExample {
fun action(arg: Any) {
println(arg)
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Swift
class SwiftExample {
init() {
print("init on \(Thread.current)")
}
deinit {
print("deinit on \(Thread.current)")
}
}
func test() {
KotlinExample().action(arg: SwiftExample())
}
|
输出结果如下:
1
2
3
| init on <_NSMainThread: 0x600003bc0000>{number = 1, name = main}
shared.SwiftExample
deinit on <_NSMainThread: 0x600003bc0000>{number = 1, name = main}
|
在以下情况下,Swift/Objective-C 对象的析构会改在特殊的 GC 线程上调用:
- Swift/Objective-C 对象是在非主线程上传给 Kotlin 的。
- 主调度队列没有被处理。
如果你想显式地在特殊 GC 线程上进行析构,请在 gradle.properties 中设置 kotlin.native.binary.objcDisposeOnMain=false。该选项会启用特殊 GC 线程上的析构,即使 Swift/Objective-C 对象是在主线程上传给 Kotlin 的。
特殊 GC 线程符合 Objective-C 运行时的要求,也就是说它有 run loop 并会排空自动释放池。
完成处理器
从 Swift 调用 Kotlin 挂起函数时,完成处理器可能会在非主线程上被调用,例如:
1
2
3
4
5
6
7
8
9
| // Kotlin
// coroutineScope、launch 和 delay 来自 kotlinx.coroutines
suspend fun asyncFunctionExample() = coroutineScope {
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
|
1
2
3
4
5
6
7
| // Swift
func test() {
print("Running test on \(Thread.current)")
PlatformKt.asyncFunctionExample(completionHandler: { _ in
print("Running completion handler on \(Thread.current)")
})
}
|
输出结果如下:
1
2
3
4
| Running test on <_NSMainThread: 0x600001b100c0>{number = 1, name = main}
Hello
World!
Running completion handler on <NSThread: 0x600001b45bc0>{number = 7, name = (null)}
|
垃圾回收与生命周期
对象的回收
对象只有在垃圾回收期间才会被回收。这同样适用于跨越互操作边界进入 Kotlin/Native 的 Swift/Objective-C 对象,例如:
1
2
3
4
5
6
| // Kotlin
class KotlinExample {
fun action(arg: Any) {
println(arg)
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Swift
class SwiftExample {
deinit {
print("SwiftExample deinit")
}
}
func test() {
swiftTest()
kotlinTest()
}
func swiftTest() {
print(SwiftExample())
print("swiftTestFinished")
}
func kotlinTest() {
KotlinExample().action(arg: SwiftExample())
print("kotlinTest finished")
}
|
输出结果如下:
1
2
3
4
5
6
| shared.SwiftExample
SwiftExample deinit
swiftTestFinished
shared.SwiftExample
kotlinTest finished
SwiftExample deinit
|
Objective-C 对象的生命周期
Objective-C 对象的存活时间可能比应有的更长,这有时会导致性能问题。例如,当一个长时间运行的循环在每次迭代中都创建若干跨越 Swift/Objective-C 互操作边界的临时对象时。
在 GC 日志中,根集合里会有若干稳定的引用(stable refs)。如果这个数字持续增长,可能说明 Swift/Objective-C 对象在应当释放时没有被释放。在这种情况下,请尝试在进行互操作调用的循环体周围加上 autoreleasepool 块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Kotlin
fun growingMemoryUsage() {
repeat(Int.MAX_VALUE) {
NSLog("$it\n")
}
}
fun steadyMemoryUsage() {
repeat(Int.MAX_VALUE) {
autoreleasepool {
NSLog("$it\n")
}
}
}
|
Swift 与 Kotlin 对象引用链的垃圾回收
考虑以下示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Kotlin
interface Storage {
fun store(arg: Any)
}
class KotlinStorage(var field: Any? = null) : Storage {
override fun store(arg: Any) {
field = arg
}
}
class KotlinExample {
fun action(firstSwiftStorage: Storage, secondSwiftStorage: Storage) {
// 这里我们创建了以下引用链:
// firstKotlinStorage -> firstSwiftStorage -> secondKotlinStorage -> secondSwiftStorage.
val firstKotlinStorage = KotlinStorage()
firstKotlinStorage.store(firstSwiftStorage)
val secondKotlinStorage = KotlinStorage()
firstSwiftStorage.store(secondKotlinStorage)
secondKotlinStorage.store(secondSwiftStorage)
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // Swift
class SwiftStorage : Storage {
let name: String
var field: Any? = nil
init(_ name: String) {
self.name = name
}
func store(arg: Any) {
field = arg
}
deinit {
print("deinit SwiftStorage \(name)")
}
}
func test() {
KotlinExample().action(
firstSwiftStorage: SwiftStorage("first"),
secondSwiftStorage: SwiftStorage("second")
)
}
|
从日志中出现 “deinit SwiftStorage first” 到出现 “deinit SwiftStorage second” 之间会有一段时间间隔。原因是 firstKotlinStorage 和 secondKotlinStorage 是在不同的 GC 周期中被回收的。事件顺序如下:
KotlinExample.action 执行完毕。由于没有任何东西引用 firstKotlinStorage,它被视为“已死”,而 secondKotlinStorage 不然,因为它被 firstSwiftStorage 引用着。- 第一个 GC 周期开始,
firstKotlinStorage 被回收。 - 此时没有对
firstSwiftStorage 的引用,所以它也被视为“已死”,于是调用 deinit。 - 第二个 GC 周期开始。由于
firstSwiftStorage 不再引用 secondKotlinStorage,后者被回收。 secondSwiftStorage 最终被回收。
回收这四个对象需要两个 GC 周期,因为 Swift 和 Objective-C 对象的析构发生在 GC 周期之后。这一限制源于 deinit,它可以调用任意代码,包括无法在 GC 暂停期间运行的 Kotlin 代码。
循环引用
在_循环引用(retain cycle)_中,若干对象通过强引用循环地互相引用:
graph TD
A --> B
B --> C
C --> AKotlin 的追踪式 GC 与 Objective-C 的 ARC 对循环引用的处理方式不同。当对象变为不可达时,Kotlin 的 GC 能正确回收这类循环,而 Objective-C 的 ARC 不能。因此,Kotlin 对象的循环引用可以被回收,而 Swift/Objective-C 对象的循环引用则不能。
考虑循环引用同时包含 Objective-C 对象和 Kotlin 对象的情况:
graph TD
Kotlin.A --> ObjC.B
ObjC.B --> Kotlin.A这涉及把 Kotlin 与 Objective-C 的内存管理模型组合在一起,而它们都无法共同处理(回收)循环引用。这意味着只要其中存在至少一个 Objective-C 对象,整个对象图的循环引用就无法被回收,也不可能从 Kotlin 一侧打破该循环。
遗憾的是,目前还没有专门的工具能自动检测 Kotlin/Native 代码中的循环引用。要避免循环引用,请使用弱引用或无主引用。
对后台状态和 App Extension 的支持
当前的内存管理器默认不跟踪应用状态,也不开箱即用地与 App Extension 集成。
这意味着内存管理器不会相应地调整 GC 行为,在某些情况下这可能是有害的。要改变这一行为,请把以下实验性二进制选项添加到你的 gradle.properties:
kotlin.native.binary.appStateTracking=enabled
它会在应用处于后台时关闭基于定时器的垃圾回收器调用,因此 GC 只会在内存消耗过高时才被调用。
接下来做什么
进一步了解 Swift/Objective-C 互操作。