Swiftが自動参照カウント(ARC)を導入する前、開発者は手動でretain、release、およびautorelease呼び出しを使用してメモリを管理しており、その結果、頻繁にメモリリークやダングリングポインタが発生していました。SwiftのARCはコンパイル時にretain/releaseの呼び出しを挿入してこれを自動化しましたが、周囲の変数をキャプチャする参照型であるクロージャとの微妙な複雑さを導入しました。これは、Swift特有のメモリ問題の新たなクラスを生み出し、2つの参照型が破壊不可能な循環依存関係を形成する可能性があり、キャプチャセマンティクスに対する明示的な制御を提供するために導入されたキャプチャリスト構文が必要でした。
クラスインスタンスがクロージャをプロパティとして保持し、そのクロージャがselfまたは他のインスタンスプロパティを参照すると、ARCはクロージャのライフタイムのためにインスタンスの参照カウントを増やします。クロージャはインスタンスによって強く参照されているため、リテインサイクルが発生します:インスタンスはクロージャを強く保持し、クロージャはインスタンスを強く保持します。どちらの参照カウントもゼロに達せず、deinitが実行されないため、アプリケーションのライフタイムにわたってメモリがリークします。
Swiftはキャプチャリストを提供します—クロージャのパラメータリストの前にある角括弧内のコンマ区切りの式で、デフォルトのキャプチャ動作を変更します。[weak self]を指定すると弱い参照が作成され(オプションで、解放されるとnilになります)、[unowned self]を指定すると所有権を持たない参照が作成されます(存在を想定し、解放後にアクセスされるとクラッシュします)。値のためには、[x = x]が現在の値をキャプチャし、参照ではなくなります。これにより、強い参照のサイクルが明示的に解消され、外部参照が削除されるとARCがインスタンスを解放できるようになります。
コードの例:
class DataManager { var completionHandler: ((Data) -> Void)? var data: Data = Data() func fetchData() { // リテインサイクル:selfがクロージャを保持し、クロージャがselfを保持する completionHandler = { newData in self.data = newData // selfの強いキャプチャ } } func fetchDataFixed() { // 解決策:弱いキャプチャ completionHandler = { [weak self] newData in guard let self = self else { return } self.data = newData } } deinit { print("DataManagerが解放されました") } }
あるプロダクションのiOSアプリケーションでは、ProfileViewControllerを実装し、プロファイルデータを非同期で取得するためにUserServiceクラスに依存しました。このサービスは、キャンセル可能なリクエストをサポートするためにプロパティとして保持されたクロージャベースの完了ハンドラを使用してAPIを公開しました。プロファイル画面から離れるとViewControllerのdeinitがトリガーされず、Instrumentsはビュー階層を保持する持続的なメモリグラフオブジェクトを報告しました。
リークを解決するためにいくつかのアーキテクチャアプローチを検討しました。
viewWillDisappearで明示的に完了ハンドラをnilに設定しようとしました。これにより、ユーザーが戻るときにサイクルが技術的に破られますが、急な終了や予期しない状態遷移には信頼性がなくなりました。クロージャが呼び出されず、ビューコントローラが消失イベントの前にシステムによって解放された場合にもリークが発生しました。このアプローチは過剰な防御的プログラミングを必要とし、ビューコントローラにサービスの内部状態を管理させることにより単一責任原則に違反しました。
オプションの強制展開のオーバーヘッドを回避するために、クロージャで[unowned self]を使用することを評価しました。これにより、構文のクリーンさとコストゼロの抽象化の利点が得られました。しかし、テスト中に、急速なナビゲーションによってViewControllerが解放される可能性のあるレースコンディションを発見し、コールバックが解放されたインスタンスにアクセスしようとするとクラッシュが発生しました。プロダクションでの未定義の動作のリスクは、パフォーマンスの利点を上回りました。
クロージャの入口で[weak self]を実装し、guard let self = self else { return }チェックを組み合わせました。これにより、すべてのライフサイクルシナリオを安全に処理できました:コールバックが発火する前にビューコントローラが解放された場合、弱い参照はnilになり、ガードが静かに失敗し、ARCがその後クロージャをクリーンアップしました。わずかに多くのボイラープレートコードが必要で、オプション処理のオーバーヘッドが導入されましたが、メモリの安全性とクラッシュのない操作を保証しました。
私たちはコードベース全体で弱いキャプチャアプローチを普遍的に採用しました。UserService統合を[weak self]を使用するようにリファクタリングした後、メモリグラフデバッグでProfileViewControllerインスタンスが解放されたことが確認されました。Xcodeのメモリグラフデバッガはクロージャからの強い参照を示さず、Instrumentsのリーク検出では機能にゼロリークが報告されました。このパターンは、すべてのクロージャベースの非同期APIの標準となりました。
クロージャ内で構造体インスタンスをキャプチャすることとクラスインスタンスをキャプチャすることはどのように異なり、構造体ではリテインサイクルを作成できないのはなぜですか?
多くの候補者は、コンテキストに関係なく、クロージャ内でselfをキャプチャすることが常にリテインサイクルのリスクであると誤って仮定します。構造体はSwiftの値型であるため、参照されるのではなくコピーされます。構造体がクロージャによってキャプチャされると、ARCは構造体の値をクロージャのキャプチャリストにコピー(または最適化に応じて不変のコピーへの参照をキャプチャ)しますが、重要なのは、構造体には参照カウントがないことです。クロージャが値を保持しているため、ヒープに割り当てられたオブジェクトへのポインタではなく、クロージャと元の構造体インスタンスとの間で循環参照が発生する可能性はありません。
危険は、selfがクラス(参照型)を指す場合にのみ存在し、クロージャがそのヒープオブジェクトへのポインタを保持し、その参照カウントを増加させます。この区別を理解することは、SwiftUIのビュー構造体とUIKitのビューコントローラでキャプチャリスト修飾子を適用するかどうかを決定するために重要です。
**[weak self]と[unowned self]の正確な違いは、オブジェクトライフタイムの仮定に関して、いつ[unowned self]**がクラッシュを引き起こすのか?
候補者はこれらを互換的に扱うことがよくあります。[weak self]は、キャプチャをオプショナルなWeakReferenceに変換し、オブジェクトが解放されるとARCが自動的にnilに設定します。それにアクセスするにはオプションバインディングが必要で、オブジェクトが死亡しても安全です。[unowned self]は、クロージャのライフタイム全体にわたってオブジェクトが存在することを想定した非所有権参照を作成します。これは、常にnilに設定されることのない暗黙的にアンラップされたオプショナルのように機能します。
クロージャがオブジェクトよりも長生きする場合(例:ビューコントローラがポップされた後に呼び出される保存された完了ハンドラ)、selfにアクセスするとダングリングポインタを解参照し、EXC_BAD_ACCESSクラッシュが発生します。クロージャとオブジェクトが同一のライフタイムを持つ場合(例えば、逃げないクロージャやクロージャがキャプチャーしたものより長生きしない特定のデリゲートパターン)にのみ、[unowned self]を使用します。
キャプチャリストは閉包スコープ外で宣言された変数とどのように相互作用し、[x]は値型のためにコピーを作成するか参照を作成しますか?
一般的な誤解は、キャプチャリストがselfにのみ影響を与えると考えられることです。{ [x] in ... }と書くと、クロージャが作成された時点のxの現在の値を明示的にキャプチャし、クロージャ内で不変のシャドウコピーを作成します。キャプチャリストなしでは、クロージャは元の変数ストレージの場所への参照をキャプチャし、クロージャが作成された後に行われた変更を観察し、xが参照型である場合に循環ロジックに参加する可能性があります。
IntやStringなどの値型の場合、[x]はコピーをキャプチャし、クロージャがxに対する外部の変更を観察できず、キャプチャ時の状態に基づいてクロージャの動作が決定論的であることを保証します。この区別は、クロージャが自分の定義スコープを超えて逃げ、元のコンテキストが変更される長い時間後に実行される場合に重要になります。