1. ホーム
  2. オブジェクティブC

[解決済み】APIを実装する際に、ブロック内でselfをキャプチャしないようにするにはどうすればいいですか?

2022-04-21 11:30:27

質問

動作中のアプリがあり、Xcode 4.2でARCに変換しているところです。事前チェックの警告のひとつに self が強く、retain cycleにつながるブロックです。この問題を説明するために、簡単なコードサンプルを作成しました。私はこの意味を理解しているつもりですが、このタイプのシナリオを実装するための正しい方法、または推奨される方法がわかりません。

  • self は MyAPI クラスのインスタンスです。
  • 以下のコードは、私の質問に関連するオブジェクトとブロックとのインタラクションのみを表示するために簡略化されています。
  • MyAPI がリモート ソースからデータを取得し、MyDataProcessor がそのデータを処理して出力を生成すると仮定します。
  • プロセッサは、進捗と状態を通信するためのブロックで構成されています。

コードサンプルです。

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [self.delegate myAPIDidFinish:self];
    self.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

質問:私は何が間違っているのでしょうか?

どのように解決するのですか?

簡単な答え

にアクセスするのではなく self を直接使用するのではなく、保持されない参照から間接的にアクセスする必要があります。 自動参照カウント(ARC)を使っていない場合 であれば、可能です。

__block MyDataProcessor *dp = self;
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

__block キーワードはブロック内で変更可能な変数をマークしますが(そんなことはしません)、ブロックが保持されるときに自動的に保持されることもありません(ARCを使用している場合を除く)。これを行う場合、MyDataProcessor インスタンスが解放された後に、他の何もブロックを実行しようとしないことを確認する必要があります。(コードの構造を考えると、それは問題ではないはずです)。 について詳しく読む __block .

ARCを使用する場合 のセマンティクスは __block が変更され、参照は保持されます。 __weak の代わりに

長い回答

例えば、こんなコードがあったとします。

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

一方、ブロックはデリゲートのプロパティを取得し、デリゲートにメソッドを送るために、selfへの参照を保持する必要があります。アプリ内の他のすべてのものがこのオブジェクトへの参照を解放しても、そのretainカウントはゼロにはならず(ブロックがそれを指しているから)、ブロックは何も間違っていない(オブジェクトがそれを指しているから)ので、オブジェクトのペアはヒープに漏れ、メモリを占有するがデバッガなしでは永遠に到達できなくなるのです。悲劇ですね、本当に。

その場合は、代わりにこうすれば簡単に解決します。

id progressDelegate = self.delegate;
self.progressBlock = ^(CGFloat percentComplete) {
    [progressDelegate processingWithProgress:percentComplete];
}

このコードでは、selfがブロックを保持し、ブロックがデリゲートを保持し、サイクルはありません(ここから見える。デリゲートは我々のオブジェクトを保持するかもしれませんが、それは今我々の手の届かないところにあります)。このコードでは、デリゲートプロパティの値は、実行時に検索されるのではなく、ブロックが作成されたときに取得されるので、同じようにリークのリスクはありません。副作用として、このブロックが作成された後にデリゲートを変更した場合、ブロックは古いデリゲートに更新メッセージを送信します。このようなことが起こる可能性があるかどうかは、アプリケーションに依存します。

仮にその動作に冷静だったとしても、あなたの場合はそのトリックを使うことはできない。

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

ここでは self をメソッド呼び出しの中で直接デリゲートに渡すので、どこかでそれを取り込まなければなりません。もしブロックタイプの定義をコントロールできるのであれば、デリゲートをパラメータとしてブロックに渡すのがベストでしょう。

self.dataProcessor.progress = ^(MyDataProcessor *dp, CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
};

このソリューションでは、retainサイクルを回避することができます は常に現在のデリゲートを呼び出します。

もしブロックを変更できないのであれば 対処法 . 保持サイクルがエラーではなく警告である理由は、必ずしもアプリケーションに破滅をもたらすとは限らないからです。もし MyDataProcessor が操作の完了時に、親がブロックを解放しようとする前にブロックを解放することができれば、サイクルは中断され、すべてが適切にクリーンアップされます。もしこのことを確認できたなら、正しいのは #pragma を使用すると、そのコードブロックに対する警告を抑制することができます。(または、ファイルごとのコンパイラ・フラグを使用します。ただし、プロジェクト全体の警告を無効にしないこと)。

また、上記のようなトリックを使って、参照をweakまたはunretainedと宣言し、ブロック内でそれを使用することも検討してみてください。例えば

__weak MyDataProcessor *dp = self; // OK for iOS 5 only
__unsafe_unretained MyDataProcessor *dp = self; // OK for iOS 4.x and up
__block MyDataProcessor *dp = self; // OK if you aren't using ARC
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

上記の3つは、少し挙動が異なりますが、いずれも結果を保持せずに参照を与えます。 __weak は、オブジェクトが解放されたときに参照をゼロにしようとします。 __unsafe_unretained は無効なポインタを残してしまいます。 __block は実際にはもう一段階間接的なレベルを追加し、 ブロックの中から参照の値を変更できるようにします (このケースでは dp は他のどこにも使われていない)。

何が ベスト は、変更できるコードとできないコードに依存します。しかし、どのように進めればよいのか、いくつかのアイデアを得ることができたと思います。