axiom-objc-block-retain-cycles について
Objective-C 開発者向けに、block の循環参照によるメモリ問題の診断と修正を案内するスキルです。診断、パターン選択、検証の手順に沿い、weak-strong パターンやネストした block、隠れた self 参照を扱います。
主な機能
- 01block の循環参照を診断
- 02weak-strong パターンの適用
- 03マクロ内の隠れた参照を確認
- 04ネストした block を階層ごとに処理
- 05オブジェクトの解放を検証
活用シーン
- 01画面を閉じてもビューコントローラーが解放されない
- 02非同期コールバックでメモリが増え続ける
- 03NSLog や NSAssert 内に self 参照がある
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-objc-block-retain-cyclesインストール後、Claude Code で/axiom-objc-block-retain-cyclesを入力して読み込まれたか確認します。
詳細
Objective-C のブロックによる保持サイクル
概要
ブロックによる保持サイクルは、Objective-C のメモリーリークの最大の原因です。ブロックが self をキャプチャし、同じオブジェクトに(直接、または operation/request を介して間接的に)保存されると、self → block → self という循環参照が発生します。基本原則 ブロックによるメモリーリークの 90%は、weak-strong パターンの欠落または誤った適用が原因であり、Apple フレームワーク自体の不具合が原因ではありません。
危険信号 — ブロックによる保持サイクルを疑う
次のいずれかが見られる場合、ほかの原因ではなく、ブロックによる保持サイクルを疑ってください。
- 通常のアプリ使用中にメモリー使用量が徐々に増え続ける
- UIViewController のインスタンスが解放されない(Instruments で確認済み)
- クラッシュ:「解放済みインスタンスへのメッセージ送信」がネットワーク/非同期コールバックで発生する
- ネットワークリクエストやアニメーションによって、ビューコントローラーを閉じられない
- ブロック内で弱参照が予期せず nil になる
- NSLog、NSAssert、文字列フォーマットによって self への参照が隠れている
- ビューコントローラーが「解放されているはず」の後に完了ハンドラーが実行される
- ❌ 禁止 「おそらく通常のメモリー使用量」として合理化する
- メモリーリークは決して「通常」ではありません
- ユーザーが画面を閉じた後、アプリのメモリー使用量は基準値に戻るべきです
- 「これで十分」や「後で監視する」として合理化しないでください
重要な区別 ブロックによる保持サイクルは、気付かれないまま蓄積します。1 つのサイクルが 100KB でも、50 画面を表示した後には、解放されないメモリーが 5MB に達します。必須:修正後はシミュレーターだけでなく、実機(サポート対象の最も古いモデル)でテストしてください。
必須の最初の手順
必ず最初に、次の操作を実行してください(コードを変更する前に)。
// 1. Identify the leak with Allocations instrument
// In Xcode: Xcode > Open Developer Tool > Instruments
// Choose Allocations template
// Perform an action (open/close a screen with the suspected block)
// Check if memory doesn't return to baseline
// Record: "Memory baseline: X MB, after action: Y MB, still allocated: Z objects"
// 2. Use Memory Debugger to trace the cycle
// Run app, pause at suspected code location
// Debug > Debug Memory Graph
// Search for the view controller that should be deallocated
// Right-click > Show memory graph
// Look for arrows pointing back to self (the cycle)
// Record: "ViewController retained by: [operation/block/property]"
// 3. Check if block is assigned to self or self's properties
// Search for: setBlock:, completion:, handler:, callback:
// Check: Is the block stored in self.property?
// Check: Is the block passed to something that retains it (network operation)?
// Record: "Block assigned to: [property or operation]"
// 4. Search for self references in the block
// Look for: [self method], self.property, self-> access
// Look for HIDDEN self references:
// - NSLog(@"Value: %@", self.property)
// - NSAssert(self.isValid, @"message")
// - Format strings: @"Name: %@", self.name
// Record: "self references found in block: [list]"
// Example output:
// Memory not returning to baseline ✓
// ViewController retained by: AFHTTPRequestOperation
// Operation retains: successBlock
// Block references self: [self updateUI], NSLog with self.property
// → DIAGNOSIS: Block retain cycle confirmed
この結果から分かること
- メモリー使用量が高いまま → 誤検知ではなく、リークが確認されています
- ViewController が operation によって保持されている → 原因はブロックです
- ブロックが self を参照している → 必要なパターンは weak-strong です
- NSLog/NSAssert に隠れた self → すべてのマクロ呼び出しを確認する必要があります
- self への参照が見つからない → ブロックによるサイクルではない可能性があります。別の原因を調査してください
必須の解釈
コードを変更する前に、次のいずれかを必ず確認してください。
- メモリー使用量が基準値に戻らず、ViewController がまだ割り当てられている場合 → ブロックによる保持サイクルが存在します
- メモリー使用量が基準値に戻る場合 → 保持サイクルではありません。ほかの原因を調査してください
- サイクルが存在するものの self への参照が見つからない場合 → 隠れた参照(マクロ、間接的なプロパティアクセス)を確認してください
- サイクルを見つけたものの参照の連鎖が分からない場合 → Memory Graph で保持されているオブジェクトをたどって、逆方向に追跡してください
診断結果に矛盾がある場合、または不明瞭な場合
- 停止してください。まだパターンの適用に進んではいけません
- 診断情報を追加してください:オブジェクトグラフを出力し、保持されているオブジェクトを一覧表示します
- 「メモリー使用量が少ないのに、なぜ ViewController はまだ割り当てられているのか」と考えてください
- メモリーグラフが分かりにくい場合は、Instruments の Leaks インストゥルメントを実行してください
判断フロー
Block memory leak suspected?
├─ Memory stays high after dismiss?
│ ├─ YES
│ │ ├─ ViewController still allocated in Memory Graph?
│ │ │ ├─ YES → Proceed to patterns
│ │ │ └─ NO → Not a block cycle, check other leaks
│ │ └─ NO → Not a leak, normal memory usage
│ │
│ └─ Crash: "Sending message to deallocated instance"?
│ ├─ Happens in block/callback?
│ │ ├─ YES → Block captured weakSelf but it became nil
│ │ │ └─ Apply Pattern 4 (Guard condition is wrong or missing)
│ │ └─ NO → Different crash, not block-related
│ └─ Crash is timing-dependent (only on device)?
│ └─ YES → Weak reference timing issue, apply Pattern 2
│
├─ Block assigned to self or self.property?
│ ├─ YES → Apply Pattern 1 (weak-strong mandatory)
│ ├─ Assigned through network operation/timer/animation?
│ │ └─ YES → Apply Pattern 1 (operation retains block indirectly)
│ └─ Block called immediately (inline execution)?
│ ├─ YES → Optional to use weak-strong (no cycle possible)
│ │ └─ But recommend for consistency with other blocks
│ └─ NO → Block stored or passed to async method → Use Pattern 1
│
├─ Multiple nested blocks?
│ └─ YES → Apply Pattern 3 (must guard ALL nested blocks)
│
├─ Block contains NSAssert, NSLog, or string format with self?
│ └─ YES → Apply Pattern 2 (macro hides self reference)
│
└─ Implemented weak-strong pattern but still leaking?
├─ Check: Is weakSelf used EVERYWHERE?
├─ Check: No direct `self` references mixed in?
├─ Check: Nested blocks also guarded?
└─ Check: No __unsafe_unretained used?
よくあるパターン
パターン選択のルール(必須)
一度に 1 つのパターンだけを、次の順序で適用します
-
必ずパターン 1(Weak-Strong の基本)から始めます
- ブロックが self または self のプロパティに割り当てられている場合 → パターン 1
- self を保持する operation/request にブロックが渡されている場合 → パターン 1
- パターン 1 を適用してもリークが続く場合に限り、パターン 2 に進みます
-
次にパターン 2(マクロ内に隠れた self)
- パターン 1 の適用後もメモリーリークが続く場合に限ります
- NSAssert、NSLog、文字列フォーマットを確認します
- 見つかった場合はパターン 2 を適用します
-
次にパターン 3(ネストされたブロック)
- ブロックにネストされたコールバックがある場合に限ります
- ネストされた各ブロックに、それぞれ独自のガードが必要です
- 見つかった場合はパターン 3 を適用します
-
次にパターン 4(ガード条件のエッジケース)
- weakSelf アプローチでクラッシュが発生する場合に限ります
- ガード条件が正しいことを確認します
- すべての箇所で strongSelf が使われていることを確認します
禁止事項
- ❌ 複数のパターンを一度に適用する
- ❌ 「weak-strong はもう分かっている」という理由でパターン 1 を飛ばす
- ❌ 回避策として __unsafe_unretained を使う
- ❌ 「今回だけ」と strong self を使う
- ❌ 「ブロックが小さいからリークにはならない」として合理化する
パターン 1: weak-strong パターン (必須)
原則 self をキャプチャするブロックは、そのブロックが self によって (直接的または間接的に) 保持される場合、必ず weak-strong パターンを使用しなければなりません。
❌ 誤り (循環参照を引き起こす)
[self.networkManager GET:@"url" success:^(id response) {
self.data = response; // self is retained by block
[self updateUI]; // block is retained by operation
} failure:^(NSError *error) {
[self handleError:error]; // CYCLE!
}];
✅ 正しい (循環を断ち切る)
__weak typeof(self) weakSelf = self;
[self.networkManager GET:@"url" success:^(id response) {
typeof(self) strongSelf = weakSelf;
if (strongSelf) {
strongSelf.data = response;
[strongSelf updateUI];
}
} failure:^(NSError *error) {
__weak typeof(self) weakSelf2 = self;
typeof(self) strongSelf = weakSelf2;
if (strongSelf) {
[strongSelf handleError:error];
}
}];
これが機能する理由
__weak typeof(self) weakSelf = self;はブロックの外側で weak 参照を作成します- ブロックは self (strong 参照) ではなく weakSelf (weak 参照) をキャプチャします
- ブロックが実行される際に、strongSelf (一時的な strong 参照) に変換します
- strongSelf が nil かどうかを確認します (オブジェクトが解放された)
- ブロックの実行中は strongSelf を使用します
- ブロックの終了時に strongSelf が解放されます → 循環は発生しません
重要な詳細
- weakSelf はブロックの内側ではなく、外側で宣言してください
- 型安全性のために
typeof(self)を使用してください (ARC と非 ARC の両方で機能します) - ガード条件は必ず
if (strongSelf)を使用し、単に宣言するだけにしないでください - weakSelf を宣言したら、ブロック内で直接
selfを使用しないでください - self をキャプチャするすべてのブロックに適用してください
selfをキャプチャするあらゆるブロックは weak-strong パターンを使用しなければなりません- これには以下が含まれます:
[self method]、self.property、self->ivar - プロパティアクセス (
self.property = value) は、メソッド呼び出しと同様に self をキャプチャします
- これには以下が含まれます:
- フレームワークに渡されるブロック:
- フレームワークのドキュメントに「ブロックは非同期で呼び出される」と記載されている場合 → weak-strong パターンを使用してください (フレームワークがブロックを保持します)
- フレームワークのドキュメントに「ブロックは即座に呼び出される」と記載されている場合 → それでも weak-strong を使用するのが安全です (より良い慣行です)
- フレームワークの動作が不明な場合 → 常に weak-strong を使用してください (害はありません)
変数のキャプチャ (間接的な self 参照を避ける)
// ✅ SAFE: Capture simple values extracted from self
__weak typeof(self) weakSelf = self;
[self.manager fetch:^(id response) {
typeof(self) strongSelf = weakSelf;
if (strongSelf) {
NSString *name = strongSelf.name; // Extract value
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Name: %@", name); // Captured the STRING, not self
});
}
}];
// ❌ WRONG: Capture properties directly in nested blocks
__weak typeof(self) weakSelf = self;
[self.manager fetch:^(id response) {
typeof(self) strongSelf = weakSelf;
if (strongSelf) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Name: %@", strongSelf.name); // Captures strongSelf again!
});
}
}];
ブロックをネストする場合は、まず単純な値を抽出し、それを内側のブロックに渡してください。これにより、プロパティアクセスを通じた self の間接的なキャプチャが発生するのを避けられます。
所要時間 ブロックあたり 30 秒
パターン 2: マクロに隠れた self
原則 NSAssert、NSLog、文字列フォーマットなどのマクロは、密かに self をキャプチャする可能性があります。それらを確認する必要があります。
❌ 誤り (NSAssert が self をキャプチャする)
[self.button setTapAction:^{
NSAssert(self.isValidState, @"State must be valid"); // self captured!
[self doWork]; // Another self reference
}];
// Leak exists even though you think only [self doWork] captures self
✅ 正しい (隠れたキャプチャを確認する)
__weak typeof(self) weakSelf = self;
[self.button setTapAction:^{
typeof(self) strongSelf = weakSelf;
if (strongSelf) {
// NSAssert still references self indirectly through strongSelf
NSAssert(strongSelf.isValidState, @"State must be valid");
[strongSelf doWork];
}
}];
よくある隠れた self 参照
NSAssert(self.condition, ...)→ 代わりに strongSelf を使用してくださいNSLog(@"Value: %@", self.property)→ strongSelf.property を使用してくださいNSError *error = [NSError errorWithDomain:@"MyApp" ...]→ 安全です、self をキャプチャしません- 文字列フォーマット:
@"Name: %@", self.name→ strongSelf.name を使用してください - インライン条件式:
self.flag ? @"yes" : @"no"→ strongSelf.flag を使用してください
それらを見つける方法
- ブロック内のすべての
self.のインスタンスを検索します - それらをマークします:
[self method]、self.property、self->ivar - それらがマクロ呼び出し (NSAssert、NSLog など) の内側にあるかどうかを確認します
- strongSelf に置き換えます
所要時間 ブロックあたり 1 分で監査
パターン 3: ネストされたブロック (それぞれにガードが必要)
原則 ネストされたブロックは連鎖を作ります: 外側のブロックが self をキャプチャし、内側のブロックが外側のブロック変数 (strongSelf を保持している) をキャプチャし、新たな循環を作ります。各ネストされたブロックには、それぞれ独自の weak-strong パターンが必要です。
❌ 誤り (外側のブロックのみをガードしている)
__weak typeof(self) weakSelf = self;
[self.manager fetchData:^(NSArray *result) {
typeof(self) strongSelf = weakSelf;
if (strongSelf) {
// Inner block captures strongSelf!
[strongSelf.analytics trackEvent:@"Fetched"
completion:^{
strongSelf.cachedData = result; // Still strong reference!
[strongSelf updateUI];
}];
}
}];
✅ 正しい (すべてのネストされたブロックをガードする)
__weak typeof(self) weakSelf = self;
[self.manager fetchData:^(NSArray *result) {
typeof(self) strongSelf = weakSelf;
if (strongSelf) {
// Declare new weak reference for inner block
__weak typeof(strongSelf) weakSelf2 = strongSelf;
[strongSelf.analytics trackEvent:@"Fetched"
completion:^{
typeof(strongSelf) strongSelf2 = weakSelf2;
if (strongSelf2) {
strongSelf2.cachedData = result;
[strongSelf2 updateUI];
}
}];
}
}];
これが機能する理由
- 各ネストレベルには、それぞれ独自の weakSelf/strongSelf のペアが必要です
- 外側のブロック: weakSelf → strongSelf
- 内側のブロック: weakSelf2 → strongSelf2
- 各レベルは独立しており安全です
重要な詳細
- ネストされたブロックで同じ weakSelf 変数を再利用しないでください
- 各ネストレベルには新しいペア (weakSelf2、strongSelf2) を使用してください
- ガード条件は各レベルで必須です
- 一貫した命名を使用してください: weakSelf、weakSelf2、weakSelf3 (可読性のため)
パターン 3 を必要とする一般的なネストされたブロックパターン
- コールバック内の完了ハンドラ
dispatch_async(queue, ^{ ... })dispatch_after(time, queue, ^{ ... })[NSTimer scheduledTimerWithTimeInterval:... block:^{ ... }][UIView animateWithDuration:... animations:^{ ... }]
これらのそれぞれは strongSelf をキャプチャする可能性のあるブロックであり、それぞれ独自の weak-strong パターンが必要です。
dispatch_async を使用した例
__weak typeof(self) weakSelf = self;
[self.manager fetchData:^(NSArray *result) {
typeof(self) strongSelf = weakSelf;
if (strongSelf) {
__weak typeof(strongSelf) weakSelf2 = strongSelf;
dispatch_async(dispatch_get_main_queue(), ^{
typeof(strongSelf) strongSelf2 = weakSelf2;
if (strongSelf2) {
strongSelf2.data = result;
[strongSelf2 updateUI];
}
});
}
}];
所要時間 ネストレベルごとに1分
パターン 4:ガード条件のエッジケース
原則 ガード条件 if (strongSelf) が正しくなければなりません。よくある間違いは、ガードを忘れる、条件を誤る、または self と strongSelf を混在させることです。
❌ 誤り(複数のガードの失敗)
__weak typeof(self) weakSelf = self;
[self.button setTapAction:^{
typeof(self) strongSelf = weakSelf;
// MISTAKE 1: Forgot guard condition
self.counter++; // CRASH! self is deallocated, accessing freed object
// MISTAKE 2: Guard exists but used wrong variable
if (weakSelf) {
[weakSelf doWork]; // weakSelf is weak, might become nil again
}
// MISTAKE 3: Mixed self and strongSelf
if (strongSelf) {
self.flag = YES; // Used self instead of strongSelf!
[strongSelf doWork];
}
}];
✅ 正しい(適切なガードと一貫した使用)
__weak typeof(self) weakSelf = self;
[self.button setTapAction:^{
typeof(self) strongSelf = weakSelf;
if (strongSelf) {
// CORRECT: Use strongSelf everywhere, never self
strongSelf.counter++;
strongSelf.flag = YES;
[strongSelf doWork];
}
// If strongSelf is nil, entire block skips gracefully
}];
これが機能する理由
if (strongSelf)はオブジェクトがまだ存在するかどうかを確認します- 存在する場合、strongSelf は強参照です(安全です)
- 存在しない場合(オブジェクトが解放済みの場合)、ブロックはスキップされます
- すべての箇所で strongSelf を使うことで、意図しない self 参照を防げます
重要なルール(必須、例外なし)
- ✅ 使用前に必ず
if (strongSelf)を確認する - ✅ if ブロック内では必ず strongSelf を使い、直接 self を使わない
- ✅ strongSelf はブロックのスコープ全体で有効であることが保証されています
- ❌ 絶対に
if (!strongSelf) return;を使わない(ロジックが紛らわしくなります) - ❌「コードを節約する」ためにガードを省略しない
- ❌ weakSelf と strongSelf へのアクセスを混在させない
- ❌ ガードなしで strongSelf を使わない(必ずクラッシュします)
間違えた場合に起きること
- ガードなし:「解放済みインスタンスにメッセージを送信しています」というエラーでクラッシュします
- 条件が誤っている場合:オブジェクトが解放されたままになり、やはりクラッシュします
- self と strongSelf の混在:意図しない self が1つあるだけで、パターン全体が無効になります
- ガードなしで strongSelf を使用:オブジェクトが解放されていると必ずクラッシュします
ガード内
if (strongSelf) {
strongSelf.data1 = value1;
[strongSelf doWork1];
[strongSelf doWork2]; // All safe
}
// ❌ WRONG: Using strongSelf after guard ends
strongSelf.data = value2; // CRASH! Outside guard
してはいけないこと
// ❌ FORBIDDEN: strongSelf without guard guarantees crash
typeof(self) strongSelf = weakSelf;
strongSelf.data = value; // CRASH if weakSelf is nil!
// ✅ MANDATORY: Always guard before using strongSelf
if (strongSelf) {
strongSelf.data = value; // Safe
}
所要時間 ガードが正しいか、ブロックごとに10秒かけて確認します
クイックリファレンス表
| 問題 | 確認事項 | 修正方法 |
|---|---|---|
| メモリ使用量がベースラインに戻らない | メモリグラフに ViewController がまだ存在していますか? | パターン 1(weak-strong)を適用します |
| クラッシュ:「解放済みインスタンスへのメッセージ」 | ガード条件が抜けているか、誤っていませんか? | パターン 4(正しいガード)を適用します |
| weak-strong を適用してもリークする | self への参照はすべて strongSelf を使っていますか? | self と strongSelf の混在がないか確認します |
| ブロックに NSAssert または NSLog が含まれている | それらは self を参照していますか? | パターン 2(マクロ内で strongSelf を使用)を適用します |
| ネストされたブロック | すべてのレベルで weak-strong を適用していますか? | パターン 3(各ブロックにガードを適用)を適用します |
| ブロックが循環参照を作るかどうかわからない | ブロックは self または self.property に代入されていますか? | はいの場合は、パターン 1 を適用します |
30分経っても行き詰まっている場合
30分以上かけてもリークが解消されない場合:
作業を止めてください。次のいずれかです
- 必須の診断手順を省略した(最もよくある原因)
- すべてのブロックに weak-strong を適用していない(ネストされたブロックを見落としている)
- 隠れた self 参照がある(NSAssert、NSLog、文字列フォーマット)
- パターンを適用したものの、直接
self参照を混在させている - 別の種類のリークがある(ブロックに関連しない)
「スキルが機能しなかった」と主張する前に必ず確認するチェックリスト
- 4つの診断ブロック(Allocations、Memory Graph、ブロック検索、self 参照検索)をすべて実行した
- Instruments でメモリ使用量がベースラインに戻らないことを確認した
- ViewController がまだ割り当てられている(解放されていない)ことを確認した
- 保持チェーン(ViewController を保持しているもの)を追跡した
- self をキャプチャするブロックをすべて見つけた(ファイル内で
[selfをグローバル検索) - 隠れた self 参照(NSAssert、NSLog、文字列フォーマット)を確認した
- 外側のブロックに weak-strong パターンを適用した
- ネストされたブロックにも weak-strong パターンを適用した(すべてのネストレベル)
- 直接の
self参照が残っていない(strongSelf のみ)ことを確認した - Instruments を再度実行し、メモリ使用量がベースラインに戻った
- シミュレーターだけでなく、実機でテストした
- 実行の合間に Xcode の派生データを消去した
すべてにチェックが付いているのに、まだリークする場合
- ブロック以外のリーク(Core Data、タイマー、デリゲート、通知)があります
- Instruments > Leaks インストゥルメントを使って、実際の循環参照を特定します
- 2~3分間プロファイルします:画面を開き、閉じる操作を5回繰り返します
- 「Leaks」パネルを確認します。解放されていないものが正確に表示されます
- 所要時間:本当の原因を特定するまで15~30分
ブロックのリークではないと判明した場合
- 「ブロックは問題なさそうだから、このままリリースしよう」と正当化しないでください
- 実際のサイクルを見つけてください(デリゲート、タイマー、プロパティオブザーバー、通知などが原因の可能性があります)
- 誤検知ではなく、実際の問題を修正してください
所要時間の目安
- パターン 1:ブロックごとに 30 秒
- パターン 2:ブロックごとに 1 分(隠れた self を監査)
- パターン 3:ネストのレベルごとに 1 分
- 行き詰まった場合のネスト診断:15~30 分
- 単純なリークの合計:5~10 分
よくある間違い
❌ ガード条件を忘れる
strongSelf.property = value;をif (strongSelf)なしで使う- オブジェクトが解放されたときにクラッシュする
- 修正:必ず
if (strongSelf) { ... }を使ってください
❌ 同じブロック内で self と strongSelf を混在させる
self.flag = YES; [strongSelf doWork];selfを直接参照すると、このパターンの効果がなくなります- 修正:ブロック内では strongSelf だけを使ってください
❌ 外側のブロックにだけパターンを適用する
- ネストしたブロックは依然として strongSelf を強参照します
- リークは解消されません
- 修正:すべてのブロックに weak-strong パターンを適用してください
❌ 「回避策」として __unsafe_unretained を使う
- ❌ 禁止パターンです。安全ではなく、クラッシュの原因になります
- オブジェクトが解放されたときにクラッシュします
- 解決策ではなく、問題を悪化させます
- 修正:代わりに weak-strong パターンを使ってください
❌ 隠れた self の参照を確認しない
- ブロック内の
NSLog(@"Value: %@", self.property) - weak-strong を適用した後もリークが残ります
- 修正:NSAssert、NSLog、文字列フォーマットを監査してください
❌ 「小さなリークだから」と正当化する
- ブロック 1 つのリークでも 100KB になることがあります
- 画面が 50 個あれば、累積して 5MB になります
- 最終的にメモリ不足でアプリがクラッシュします
- 修正:すべてのブロックリークを修正し、正当化しないでください
❌ システムフレームワーク内のブロックは安全だと思い込む
- UIView アニメーション、AFNetworking、dispatch、タイマー
- いずれも self を参照するブロックを保持する可能性があります
- 修正:参照元にかかわらず、weak-strong パターンを適用してください
❌ シミュレーターだけでテストする
- シミュレーターのメモリプレッシャーは実機と異なります
- 実機に負荷がかかるまでリークが現れないことがあります
- 修正:実機で、サポート対象の最も古いモデルを使ってテストしてください
実際の影響
導入前 ブロックのメモリリークのデバッグに問題ごとに 2~3 時間
- Allocations を実行しても、どこを見ればよいかわからない
- あちこちを検索しても、明確な診断手順がない
- 手当たり次第に修正を試し、どれかが効くことを期待する
- サンクコストの誤謬に陥り、そのままリリースする
- 顧客からクラッシュや動作低下の報告が届く
導入後 系統的な診断により 5~10 分
- Allocations を実行し、メモリがベースラインに戻らないことを確認する
- Memory Graph で何が保持されているかを正確に確認する
- グローバル検索で self をキャプチャするブロックをすべて見つける
- weak-strong パターンを適用する(ブロックごとに 30 秒)
- Instruments でテストし、メモリがベースラインに戻ることを確認する
- 完了
重要なポイント ブロックの保持サイクルは、weak-strong パターンで 100% 防止できます。例外や、強参照してもよい「特別なケース」はありません。
最終更新日:2025-11-30 ステータス:負荷シナリオで TDD テスト済み フレームワーク:Objective-C、ブロック(クロージャ)、ARC