1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Combineと弱参照:安全な非同期処理のためのメモリ管理

Posted at

Combineは、Swiftにおける非同期イベント処理のための強力なフレームワークです。非同期処理が絡むと、メモリ管理の問題―特に循環参照によるメモリリーク―が発生するリスクも高まります。この記事では、Combineと弱参照(weak reference)の関係について詳しく解説し、どのようにして安全なコードを書くかを紹介します。

ARCと循環参照の問題

Swiftでは、ARC(自動参照カウント)がメモリ管理を担っています。オブジェクトは、どこからも参照されなくなった時点で解放される仕組みになっています。しかし、もし複数のオブジェクトが互いに強参照(strong reference)しあっている場合、参照カウントが0にならず、使われなくなったオブジェクトがメモリ上に残り続ける循環参照が発生してしまいます。

例えば、以下のようなコードでは、AとBがお互いを強参照しているため、doブロックを抜けても解放されません。

class A {
    var b: B?
}

class B {
    var a: A?
}

do {
    let a = A()
    let b = B()
    a.b = b
    b.a = a
} // ここでaとbは互いに強参照しているため解放されない

Combineにおける弱参照の必要性

Combineを利用する際、よく使われるオペレーターにはsinkやassign(to: on:)などがあります。これらのオペレーターは、内部でクロージャを用いてイベントを処理します。クロージャ内でself(親クラス)を直接参照すると、クロージャがselfを強参照してしまい、self自身もクロージャ(またはその管理下にあるAnyCancellable)を保持してしまいます。結果として、双方が解放されず、メモリリークを引き起こす原因となります。

例:sinkオペレーター内での弱参照の実装

以下のコードは、MyViewModelクラス内でCombineのsinkオペレーターを使い、selfを弱参照([weak self])でキャプチャする例です。

import Combine

class MyViewModel {
    private var cancellable: AnyCancellable?
    
    func startListening() {
        cancellable = somePublisher
            .sink { [weak self] value in
                guard let self = self else { return }
                self.handle(value)
            }
    }
    
    private func handle(_ value: SomeType) {
        // 値に応じた処理
    }
}

この例では、クロージャ内でselfを弱参照としてキャプチャすることで、MyViewModelが不要になったときに正しく解放されるようにしています。

例:ObservableObjectとCombineで弱参照を利用する

Combineでは、SwiftUIと連携してObservableObjectを用いることも多いです。以下は、ObservableObjectに準拠したクラスが、Timerを使って定期的に値を更新する例です。

import Combine
import Foundation

// 発行するデータの型
struct SomeType {
    let value: String
}

// ObservableObjectに準拠したパブリッシャークラス
class SomePublisher: ObservableObject {
    // 値が更新されるたびに購読者に通知される
    @Published var currentValue: SomeType = SomeType(value: "Initial Value")
    
    private var timerSubscription: AnyCancellable?
    
    init() {
        // Timerパブリッシャーを用いて1秒ごとにSomeTypeを発行する
        timerSubscription = Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect() // タイマーを自動で接続
            .map { _ in
                SomeType(value: "Updated at \(Date())")
            }
            .assign(to: \.currentValue, on: self)
    }
    
    deinit {
        timerSubscription?.cancel()
    }
}

この例では、SomePublisherが1秒ごとに新しい値を生成し、@Publishedプロパティを通じてSwiftUIのViewなどに通知します。ここでも、内部的にはCombineの仕組みが働いており、必要に応じて弱参照や適切なキャプチャ方法を用いることで、循環参照を防ぐことが可能です。

まとめ

Combineを利用する際、非同期イベントを扱うクロージャ内でselfを参照するときは、必ず弱参照を意識しましょう。以下がポイントです。

ARCと循環参照の基本:

ARCは参照カウントが0になった時点でオブジェクトを解放しますが、互いに強参照し合うと解放されず、メモリリークの原因となります。

Combineでの弱参照の実践:

Combineのsinkやassign(to: on:)などのオペレーターを使う際、クロージャ内でselfを強参照してしまわないよう、明示的に[weak self]や[unowned self]を用います。

デバッグと確認:

コード実装後は、Memory Graph Debuggerなどのツールで循環参照が発生していないか確認し、安全なメモリ管理が行われているかをチェックしましょう。

このように、適切な弱参照の設定は、Combineを使ったアプリケーションにおいても安全かつ効率的なメモリ管理のための重要なテクニックとなります。ぜひ、今回のポイントを参考に、クリーンなコードを書いてください!

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?