メインコンテンツまでスキップ

Swift/Objective-C ARC との統合

KotlinとObjective-Cは、異なるメモリ管理戦略を使用します。Kotlinはトレース型ガベージコレクタを持ち、一方Objective-Cは自動参照カウント(ARC)に依存しています。

通常、これらの戦略間の統合はシームレスであり、追加の作業は必要ありません。ただし、留意すべき点がいくつかあります。

スレッド

デイニシャライザ

Swift/Objective-Cのオブジェクト、およびそれらが参照するオブジェクトのデイニシャライズは、これらのオブジェクトがメインスレッドでKotlinに渡された場合、メインスレッドで呼び出されます。例:

// Kotlin
class KotlinExample {
fun action(arg: Any) {
println(arg)
}
}
// Swift
class SwiftExample {
init() {
print("init on \(Thread.current)")
}

deinit {
print("deinit on \(Thread.current)")
}
}

func test() {
KotlinExample().action(arg: SwiftExample())
}

結果の出力:

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.propertieskotlin.native.binary.objcDisposeOnMain=falseを設定してください。このオプションは、Swift/Objective-CオブジェクトがメインスレッドでKotlinに渡された場合でも、特別なGCスレッドでのデイニシャライズを有効にします。

特別なGCスレッドはObjective-C runtimeに準拠しており、ランループを持ち、autorelease poolをdrainします。

完了ハンドラ

SwiftからKotlinのsuspend関数を呼び出す場合、完了ハンドラがメインスレッド以外のスレッドで呼び出されることがあります。例:

// Kotlin
// coroutineScope, launch, and delay are from kotlinx.coroutines
suspend fun asyncFunctionExample() = coroutineScope {
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
// Swift
func test() {
print("Running test on \(Thread.current)")
PlatformKt.asyncFunctionExample(completionHandler: { _ in
print("Running completion handler on \(Thread.current)")
})
}

結果の出力:

Running test on <_NSMainThread: 0x600001b100c0>{number = 1, name = main}
Hello
World!
Running completion handler on <NSThread: 0x600001b45bc0>{number = 7, name = (null)}

ガベージコレクションとライフサイクル

オブジェクトの再利用

オブジェクトはガベージコレクション中にのみ再利用されます。これは、Interop境界を越えてKotlin/Nativeに入るSwift/Objective-Cオブジェクトに適用されます。例:

// Kotlin
class KotlinExample {
fun action(arg: Any) {
println(arg)
}
}
// 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")
}

結果の出力:

shared.SwiftExample
SwiftExample deinit
swiftTestFinished
shared.SwiftExample
kotlinTest finished
SwiftExample deinit

Objective-Cオブジェクトのライフサイクル

Objective-Cオブジェクトは必要以上に長く生存する可能性があり、パフォーマンス上の問題を引き起こすことがあります。たとえば、長時間のループで、各イテレーションでSwift/Objective-C interop境界を越える一時オブジェクトが複数作成される場合などです。

GC logsには、root set内のstable refsの数が表示されます。この数が増え続ける場合は、Swift/Objective-Cオブジェクトが解放されるべき時に解放されていないことを示している可能性があります。この場合は、Interop呼び出しを行うループ本体の周りにautoreleasepoolブロックを試してください。

// Kotlin
fun growingMemoryUsage() {
repeat(Int.MAX_VALUE) {
NSLog("$it
")
}
}

fun steadyMemoryUsage() {
repeat(Int.MAX_VALUE) {
autoreleasepool {
NSLog("$it
")
}
}
}

SwiftとKotlinオブジェクトのチェーンのガベージコレクション

次の例を考えてみましょう。

// 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) {
// Here, we create the following chain:
// firstKotlinStorage `->` firstSwiftStorage `->` secondKotlinStorage `->` secondSwiftStorage.
val firstKotlinStorage = KotlinStorage()
firstKotlinStorage.store(firstSwiftStorage)
val secondKotlinStorage = KotlinStorage()
firstSwiftStorage.store(secondKotlinStorage)
secondKotlinStorage.store(secondSwiftStorage)
}
}
// 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"のメッセージが表示されるまでに時間がかかります。その理由は、firstKotlinStoragesecondKotlinStorageが異なるGCサイクルで収集されるためです。イベントのシーケンスは次のとおりです。

  1. KotlinExample.actionが終了します。firstKotlinStorageは、何も参照していないため「dead」と見なされます。一方、secondKotlinStorageは、firstSwiftStorageによって参照されているため「dead」とは見なされません。
  2. 最初のGCサイクルが開始され、firstKotlinStorageが収集されます。
  3. firstSwiftStorageへの参照がないため、これも「dead」となり、deinitが呼び出されます。
  4. 2番目のGCサイクルが開始されます。firstSwiftStorageが参照しなくなったため、secondKotlinStorageが収集されます。
  5. 最後に、secondSwiftStorageが再利用されます。

これらの4つのオブジェクトを収集するには、2つのGCサイクルが必要です。SwiftおよびObjective-CオブジェクトのデイニシャライズはGCサイクルの後に行われるためです。この制限は、deinitに起因します。deinitは、GC一時停止中に実行できないKotlinコードを含む任意のコードを呼び出すことができます。

循環参照

_循環参照_では、複数のオブジェクトが強い参照を使用して互いに循環的に参照します。

Retain cycles

KotlinのトレースGCとObjective-CのARCは、循環参照を異なる方法で処理します。オブジェクトが到達不能になった場合、KotlinのGCはこのようなサイクルを適切に再利用できますが、Objective-CのARCはできません。したがって、Kotlinオブジェクトの循環参照は再利用できますが、Swift/Objective-Cオブジェクトの循環参照はできません

循環参照にObjective-CオブジェクトとKotlinオブジェクトの両方が含まれる場合を考えてみましょう。

Retain cycles with Objective-C and Kotlin objects

これには、KotlinとObjective-Cのメモリ管理モデルを組み合わせることが含まれ、循環参照を一緒に処理(再利用)できません。つまり、少なくとも1つのObjective-Cオブジェクトが存在する場合、オブジェクトのグラフ全体の循環参照を再利用できず、Kotlin側からサイクルを解除することは不可能です。

残念ながら、Kotlin/Nativeコードで循環参照を自動的に検出するための特別なツールは現在利用できません。循環参照を回避するには、weak または unowned 参照を使用してください。

バックグラウンド状態とApp Extensionsのサポート

現在のメモリマネージャは、デフォルトではアプリケーションの状態を追跡せず、App Extensionsとそのままでは統合されません。

これは、メモリマネージャがそれに応じてGCの動作を調整しないことを意味し、場合によっては有害となる可能性があります。この動作を変更するには、次のExperimentalバイナリオプションをgradle.propertiesに追加します。

kotlin.native.binary.appStateTracking=enabled

これにより、アプリケーションがバックグラウンドにあるときにタイマーベースのガベージコレクタの呼び出しが無効になるため、GCはメモリ消費が高くなりすぎた場合にのみ呼び出されます。

次のステップ

Swift/Objective-C interopについて詳しく学んでください。