リテインサイクル — 本質、発生原因とアプリ開発における解消

著者: IT Sectr 公開日: 2026-03-29 読了時間: 8 分

リテインサイクルはARCにおける状況で、ふたつ以上のオブジェクトがstrong参照を通じて互いに参照し合い、閉じたループを形成します。Apple Memory Management Guide, 2026によると、リテインサイクルはサイクル内のすべてのオブジェクトの解放をブロックします。なぜなら、各オブジェクトのreatin countが≥ 1だからです。GCにおけるメモリリークとは異なり、リテインサイクルは、サイクルの少なくとも一つの外部メンバーが生きている限り、オブジェクトが生きていることを保証します — そして、サイクルが独立していれば、すべての外部参照を失った後でさえもそうです。

メインポイント

  • リテインサイクル — ARCがオブジェクトを解放できないstrong参照の閉じたチェーン
  • 原因 — ふたつ(以上)のオブジェクトが互いにstrong参照を持ち、retain countをゼロにできない
  • 結果 — メモリリーク: オブジェクトが永遠にメモリに残り続け、RAM消費が増大する
  • 解決策 — サイクル内の一つのstrong参照をweakまたはunownedに置き換える
  • 診断 — Xcode Memory Debugger、Instruments Leaks、Debug Memory Graph

リテインサイクルとは?

リテインサイクルは、ふたつ以上のオブジェクトがstrong参照を通じて互いを所有し、閉じた依存グラフを作り出す状況です。ARCはこれらのオブジェクトを解放できません。なぜなら、各オブジェクトのreatin countが常に≥ 1だからです: オブジェクトAがBを保持し、BがAを保持し、それぞれのカウンターが絶対にゼロになりません。

この問題は、参照カウントシステム(ARC、MRR)にのみ発生します。ガベージコレクション(GC)では、コレクターはルートセットからの参照グラフによってアクセス不可能性を判定します — サイクルは障害ではありません。しかしARCでは、サイクルはリークと同等であり、決定論的な解放をカウントによって行うことが循環依存を解決できないからです。

WWDC 2012 Session 406によると、リテインサイクルはObjective-CおよびSwiftアプリケーションにおけるメモリリークの最もよくある原因です。よくあるシナリオ: デリゲートとの親子関係、selfをキャプチャするクロージャ、双方向の関係をもつ階層的アーキテクチャ。

iOS開発におけるリテインサイクルの例

あらゆるiOS開発者が当たる、クラシックなリテインサイクルのシナリオを検討しましょう。これらのパターンを理解することが、ARCで安全なコードを書くための基礎です。

デリゲートとの親子関係

クラシックなシナリオ: 親オブジェクト(例:UIViewController)が子オブジェクトを作成し、そのデリゲートになります。両方がstrong参照を使用すると、リテインサイクルが発生します。解決策は — デリゲートをweakにすることです。

swift
// エラー: strong delegateによるリテインサイクル
protocol ChildDelegate: AnyObject { }

class ParentVC: UIViewController, ChildDelegate {
    var child: ChildVC?

    func showChild() {
        child = ChildVC()
        child?.delegate = self        // Parent → Child (strong)
    }                                 // Child → Parent (delegateを通じてstrong)
}                                     // ⚠️ リテインサイクル!

class ChildVC: UIViewController {
    var delegate: ChildDelegate?    // ❌ デフォルトでstrong
}

// 修正: weak delegate
class ChildVC: UIViewController {
    weak var delegate: ChildDelegate? // ✅ weak — 保持しない
}

例では、ParentVCはchildプロパティを通じてChildVCに対してstrong参照を持ちます。ChildVCはdelegateを通じてParentVCに対してstrong参照を持ちます。サイクルが閉じています。修正: weak var delegate — この参照でとretain countが増加せず、ParentVCを解放できます。

NSTimerとリテインサイクル

NSTimerはリテインサイクルのクラシックな原因です。タイマーはターゲット(通常self)を保持し、ターゲットはプロパティを通じてタイマーを保持します。タイマーが一回限であっても、invalidateが呼ばれるまで解放されません。解決策: deinitまたはviewDidDisappearで必ず timer.invalidate()を呼ぶ。

階層的アーキテクチャ

ケイスケードの所有権をもつアーキテクチャ(コーディネータ、ルータ)では、多段階のサイクルがよく発生します: Coordinator → ViewController → ViewModel → Coordinator(コールバックを通じて)。チェーン内の各strong参照は意識的に選択する必要があります — どこか一かweak参照があればサイクルを解消できます。

Swiftクロージャにおけるリテインサイクル

Swiftのクロージャはstrong参照で外部変数をキャプチャします。クロージャがオブジェクトのプロパティとして保管され(例:completion handler)、かつselfをキャプチャする場合、リテインサイクルが生じます: self → closure → self

これは現代のSwift開発におけるリテインサイクルの最もよくある原因です。これは暗黙のうちに発生します — 開発者は、特に明示的なselfを使わない省略シンタックスを使用する場合、クロージャ内でのselfのキャプチャに気付けない可能性があります。

swift
class DownloadService {
    var onComplete: ((Data) -> Void)?
    var result: Data?

    func startDownload() {
        // ❌ リテインサイクル: self → onComplete → self
        onComplete = { data in
            self.result = data
            self.notifyUI()
        }

        // ✅ 修正: weak selfでのキャプチャリスト
        onComplete = { [weak self] data in
            guard let self else { return }
            self.result = data
            self.notifyUI()
        }
    }

    func notifyUI() { }
}

キャプチャリスト [weak self]は、クロージャ内でselfへの弱参照を作ります。クロージャの実行前にDownloadServiceが解放されていれば、selfはnilになり、コードはguardを通じて安全に終了します。これはSwiftの非同期クロージャにおける標準パターンです — クロージャがプロパティとして保管されるときは必ず使用する必要があります。

クロージャでのunowned self

unowned selfはweak selfの代替案で、selfがクロージャよりも長く生きていることが保証されている場合に使用します。例: 即座に実行される同期クロージャ(sorted、filter)。これらの場合、selfは確実に生きており、unownedは安全です。ただし、unownedは解放されたオブジェクトにアクセスするとクラッシュします — そのため、weakがデフォルトの安全な選択肢とされています。

リテインサイクルの検出方法: 診断ツール

アプリケーションのパフォーマンスにとって、リテインサイクルを早い段階で検出することは非常に重要です。iOS開発における循環参照を識別するための主なツールと技術を確認しましょう。

Xcode Memory Debugger

Xcode Memory Debugger(Debug Memory Graph)は、メモリ内のオブジェクトのグラフをその参照とともに表示する視覚ツールです。リテインサイクルは、strong箭の閉じたチェーンとして表示されます。起動方法: アプリ実行中にDebug areaパネルのDebug Memory Graphボタンをクリックします。各オブジェクトは、タイプ、アドレス、参照のリストとともに表示されます。

Instruments Leaks

Instruments Leaksは、リークを自動的に検出するためのプロファイラーです。アロケーションを記録し、リアルタイムで参照グラフを解析します。リテインサイクルだけでなく、忘れられた参照、解放されないViewController、その他のリークも検出します。Leaksは正確なオブジェクトと保持チェーンを指示します。

deinitログ

最も簡単な方法は、各キークラスのdeinitにprintを追加することです。オブジェクトが破壊されるはずでもdeinitが呼ばれない場合、リテインサイクルがあります。この方法はツールが不要で、初期診断に効果的です。

ツールタイプ使用するタイミング
Memory Debugger視覚グラフナビゲーション後の手動チェック
Instruments Leaks自動解析リグレッションテスト、CI
deinit print手動ログ開発、コードレビュー
Malloc Scribbleランタイムフラグuse-after-freeのデバッグ

推奨されるアプローチ: 開発中はdeinitログ、手動テスト中はMemory Debugger、自動リグレッションリーク検出のためにCI/CDパイプラインでInstruments Leaksを使用します。

リテインサイクルの防止とベストプラクティス

リテインサイクルは、本番環境で修正するよりも予防するほうが簡単です。以下は、循環参照のリスクを最小限に減らすためのいくつかのルールです。

weakデリゲートのルール

すべてのデリゲートおよびdataSourceはweakである必要があります。このルールはUIKitに組み込まれています: Apple SDK内のすべてのデリゲートプロトコルはweakプロパティで宣言されています(UITableView.delegate、UICollectionView.dataSource)。自分のプロトコルには、weak var delegate: MyDelegate?を使用し、プロトコルをAnyObjectから継承します。

クロージャでのキャプチャリスト

プロパティとして保管され、かつselfをキャプチャするすべてのクロージャ(completion handler、callback)は、キャプチャリストで[weak self]を使用する必要があります。例外は、即座に実行され、保管されないクロージャ(sorted、map、filter)です。これらにはunowned selfが安全です。

アーキテクチャのレビュー

複雑なアーキテクチャ(VIPER、Coordinators、Redux)では、strong参照の向きをトレースします。所有者は下位に対してstrong参照を持ちますが、下位はweakまたはunownedを通してのみ所有者を参照する必要があります。単方向データフローは参照管理を簡単にします。

swift
// 例: deinitログによる確認
class BaseViewController: UIViewController {
    deinit {
        print("✅ \(type(of: self)) deallocated")
    }
}

// 使用法: すべてのViewControllerがBaseViewControllerを継承
class ProfileVC: BaseViewController {
    var viewModel: ProfileViewModel?
    var onLogout: (() -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        onLogout = { [weak self] in
            self?.dismiss(animated: true)
        }
    }
}
// ProfileVCを閉じる際、コンソールに"✅ ProfileVC deallocated"が表示されることを確認

deinitログをもつベースクラスは、即座にフィードバックを提供します。画面が閉じられるはずのときにメッセージが表示されない場合、このクラスにリテインサイクルがあります。すべてのViewControllerに対して、この実践をプロジェクトテンプレートに追加します。

よくある質問

リテインサイクルとGCでのメモリリークの違いは?

リテインサイクルは、strong参照の閉じたループが解放をブロックする、ARC固有の問題です。GCでは、コレクターは参照カウントではなく、ルートセットからのアクセス可能性を解析します — ですから、サイクルはリークではありません。一方ARCでは、何らかの独立したサイクルは確実にリークとなります。

weak参照はどのようにリテインサイクルを解消するのか?

Weak参照はオブジェクトのreatin countを増加させません。サイクル内の一つのstrong参照をweakに置き換えれば、各オブジェクトのreatin countがゼロになる可能性が生じます。オブジェクトが解放された後、weak参照は自動的に nil に設定され、解放されたメモリへのアクセスを防ぎます。

リテインサイクルは3つ以上のオブジェクトから構成されることがありますか?

はい、リテインサイクルはいくつでも多くのオブジェクトを含むことができます: A → B → C → A。これを解消するには、1つのリンクを切れば良いだけです — どこかのstrong参照をweakまたはunownedに置き換えます。ツールはペアだけでなく、グラフ全体を表示します。

GCD DispatchWorkItemがリテインサイクルを作らないのはなぜか?

GCD(Grand Central Dispatch)は実行後クロージャを保持しません。DispatchWorkItemは実行され、解放されます。例えクロージャがselfをキャプチャしていてもそうです。リテインサイクルは、クロージャがプロパティとして保管されている場合(クラス内のcompletion handler)にのみ発生し、キューに渡された場合には発生しません。

Instrumentsが検出できないリテインサイクルの種類は?

Instruments Leaksは、一時的なリテインサイクル(数秒だけ続く)や、ブリッジングを通じたC/C++オブジェクト内の循環参照を常に見つけられるわけではありません。完全な確認には、シーン内のすべてのキーオブジェクトのdeinitログと並んで Memory Debuggerを手動で使用します。

まとめ

  • リテインサイクル — ARCにおけるオブジェクトの解放をブロックするstrong参照の閉じたチェーン
  • 原因 — strong参照のデリゲート、selfをキャプチャするクロージャ、双方向の親子関係
  • 解決策 — 1つのstrong参照をweakまたはunownedに置き換えればサイクルが解消される
  • クロージャ — 保管された completion handler は必ず [weak self] を使用
  • デリゲート — 常にweak; デリゲートプロトコルはAnyObjectから継承
  • 検出 — Xcode Memory Debugger、Instruments Leaks、deinitログ
  • 予防 — 単方向データフロー、weakデリゲート、キャプチャリスト、deinitをもつベースクラス

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください