リテインサイクルはARCにおける状況で、ふたつ以上のオブジェクトがstrong参照を通じて互いに参照し合い、閉じたループを形成します。Apple Memory Management Guide, 2026によると、リテインサイクルはサイクル内のすべてのオブジェクトの解放をブロックします。なぜなら、各オブジェクトのreatin countが≥ 1だからです。GCにおけるメモリリークとは異なり、リテインサイクルは、サイクルの少なくとも一つの外部メンバーが生きている限り、オブジェクトが生きていることを保証します — そして、サイクルが独立していれば、すべての外部参照を失った後でさえもそうです。
メインポイント
リテインサイクルは、ふたつ以上のオブジェクトがstrong参照を通じて互いを所有し、閉じた依存グラフを作り出す状況です。ARCはこれらのオブジェクトを解放できません。なぜなら、各オブジェクトのreatin countが常に≥ 1だからです: オブジェクトAがBを保持し、BがAを保持し、それぞれのカウンターが絶対にゼロになりません。
この問題は、参照カウントシステム(ARC、MRR)にのみ発生します。ガベージコレクション(GC)では、コレクターはルートセットからの参照グラフによってアクセス不可能性を判定します — サイクルは障害ではありません。しかしARCでは、サイクルはリークと同等であり、決定論的な解放をカウントによって行うことが循環依存を解決できないからです。
WWDC 2012 Session 406によると、リテインサイクルはObjective-CおよびSwiftアプリケーションにおけるメモリリークの最もよくある原因です。よくあるシナリオ: デリゲートとの親子関係、selfをキャプチャするクロージャ、双方向の関係をもつ階層的アーキテクチャ。
あらゆるiOS開発者が当たる、クラシックなリテインサイクルのシナリオを検討しましょう。これらのパターンを理解することが、ARCで安全なコードを書くための基礎です。
クラシックなシナリオ: 親オブジェクト(例:UIViewController)が子オブジェクトを作成し、そのデリゲートになります。両方がstrong参照を使用すると、リテインサイクルが発生します。解決策は — デリゲートをweakにすることです。
// エラー: 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はリテインサイクルのクラシックな原因です。タイマーはターゲット(通常self)を保持し、ターゲットはプロパティを通じてタイマーを保持します。タイマーが一回限であっても、invalidateが呼ばれるまで解放されません。解決策: deinitまたはviewDidDisappearで必ず timer.invalidate()を呼ぶ。
ケイスケードの所有権をもつアーキテクチャ(コーディネータ、ルータ)では、多段階のサイクルがよく発生します: Coordinator → ViewController → ViewModel → Coordinator(コールバックを通じて)。チェーン内の各strong参照は意識的に選択する必要があります — どこか一かweak参照があればサイクルを解消できます。
Swiftのクロージャはstrong参照で外部変数をキャプチャします。クロージャがオブジェクトのプロパティとして保管され(例:completion handler)、かつselfをキャプチャする場合、リテインサイクルが生じます: self → closure → self。
これは現代のSwift開発におけるリテインサイクルの最もよくある原因です。これは暗黙のうちに発生します — 開発者は、特に明示的なselfを使わない省略シンタックスを使用する場合、クロージャ内でのselfのキャプチャに気付けない可能性があります。
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はweak selfの代替案で、selfがクロージャよりも長く生きていることが保証されている場合に使用します。例: 即座に実行される同期クロージャ(sorted、filter)。これらの場合、selfは確実に生きており、unownedは安全です。ただし、unownedは解放されたオブジェクトにアクセスするとクラッシュします — そのため、weakがデフォルトの安全な選択肢とされています。
アプリケーションのパフォーマンスにとって、リテインサイクルを早い段階で検出することは非常に重要です。iOS開発における循環参照を識別するための主なツールと技術を確認しましょう。
Xcode Memory Debugger(Debug Memory Graph)は、メモリ内のオブジェクトのグラフをその参照とともに表示する視覚ツールです。リテインサイクルは、strong箭の閉じたチェーンとして表示されます。起動方法: アプリ実行中にDebug areaパネルのDebug Memory Graphボタンをクリックします。各オブジェクトは、タイプ、アドレス、参照のリストとともに表示されます。
Instruments Leaksは、リークを自動的に検出するためのプロファイラーです。アロケーションを記録し、リアルタイムで参照グラフを解析します。リテインサイクルだけでなく、忘れられた参照、解放されないViewController、その他のリークも検出します。Leaksは正確なオブジェクトと保持チェーンを指示します。
最も簡単な方法は、各キークラスのdeinitにprintを追加することです。オブジェクトが破壊されるはずでもdeinitが呼ばれない場合、リテインサイクルがあります。この方法はツールが不要で、初期診断に効果的です。
| ツール | タイプ | 使用するタイミング |
|---|---|---|
| Memory Debugger | 視覚グラフ | ナビゲーション後の手動チェック |
| Instruments Leaks | 自動解析 | リグレッションテスト、CI |
| deinit print | 手動ログ | 開発、コードレビュー |
| Malloc Scribble | ランタイムフラグ | use-after-freeのデバッグ |
推奨されるアプローチ: 開発中はdeinitログ、手動テスト中はMemory Debugger、自動リグレッションリーク検出のためにCI/CDパイプラインでInstruments Leaksを使用します。
リテインサイクルは、本番環境で修正するよりも予防するほうが簡単です。以下は、循環参照のリスクを最小限に減らすためのいくつかのルールです。
すべてのデリゲートおよび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を通してのみ所有者を参照する必要があります。単方向データフローは参照管理を簡単にします。
// 例: 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に対して、この実践をプロジェクトテンプレートに追加します。
よくある質問
リテインサイクルは、strong参照の閉じたループが解放をブロックする、ARC固有の問題です。GCでは、コレクターは参照カウントではなく、ルートセットからのアクセス可能性を解析します — ですから、サイクルはリークではありません。一方ARCでは、何らかの独立したサイクルは確実にリークとなります。
Weak参照はオブジェクトのreatin countを増加させません。サイクル内の一つのstrong参照をweakに置き換えれば、各オブジェクトのreatin countがゼロになる可能性が生じます。オブジェクトが解放された後、weak参照は自動的に nil に設定され、解放されたメモリへのアクセスを防ぎます。
はい、リテインサイクルはいくつでも多くのオブジェクトを含むことができます: A → B → C → A。これを解消するには、1つのリンクを切れば良いだけです — どこかのstrong参照をweakまたはunownedに置き換えます。ツールはペアだけでなく、グラフ全体を表示します。
GCD(Grand Central Dispatch)は実行後クロージャを保持しません。DispatchWorkItemは実行され、解放されます。例えクロージャがselfをキャプチャしていてもそうです。リテインサイクルは、クロージャがプロパティとして保管されている場合(クラス内のcompletion handler)にのみ発生し、キューに渡された場合には発生しません。
Instruments Leaksは、一時的なリテインサイクル(数秒だけ続く)や、ブリッジングを通じたC/C++オブジェクト内の循環参照を常に見つけられるわけではありません。完全な確認には、シーン内のすべてのキーオブジェクトのdeinitログと並んで Memory Debuggerを手動で使用します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。