Wie vermeide ich es, mich bei der Implementierung einer API in Blöcken zu erfassen?

222

Ich habe eine funktionierende App und arbeite daran, sie in Xcode 4.2 in ARC zu konvertieren. Eine der Warnungen vor der Prüfung besteht darin, selfstark in einem Block zu erfassen , was zu einem Aufbewahrungszyklus führt. Ich habe ein einfaches Codebeispiel erstellt, um das Problem zu veranschaulichen. Ich glaube, ich verstehe, was dies bedeutet, bin mir aber nicht sicher, wie diese Art von Szenario "richtig" oder empfohlen implementiert werden kann.

  • self ist eine Instanz der Klasse MyAPI
  • Der folgende Code ist vereinfacht, um nur die Interaktionen mit den Objekten und Blöcken anzuzeigen, die für meine Frage relevant sind
  • Angenommen, MyAPI ruft Daten von einer Remote-Quelle ab und MyDataProcessor verarbeitet diese Daten und erzeugt eine Ausgabe
  • Der Prozessor ist mit Blöcken konfiguriert, um Fortschritt und Status zu kommunizieren

Codebeispiel:

// 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];

Frage: Was mache ich "falsch" und / oder wie sollte dies geändert werden, um den ARC-Konventionen zu entsprechen?

XJones
quelle

Antworten:

509

Kurze Antwort

Anstatt selfdirekt darauf zuzugreifen , sollten Sie indirekt über eine Referenz darauf zugreifen, die nicht beibehalten wird. Wenn Sie die automatische Referenzzählung (ARC) nicht verwenden , können Sie Folgendes tun:

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

Das __blockSchlüsselwort markiert Variablen, die innerhalb des Blocks geändert werden können (das tun wir nicht), aber sie werden auch nicht automatisch beibehalten, wenn der Block beibehalten wird (es sei denn, Sie verwenden ARC). Wenn Sie dies tun, müssen Sie sicher sein, dass nichts anderes versucht, den Block auszuführen, nachdem die MyDataProcessor-Instanz freigegeben wurde. (Angesichts der Struktur Ihres Codes sollte dies kein Problem sein.) Lesen Sie mehr darüber__block .

Wenn Sie ARC verwenden , __blockbleiben die Semantik der Änderungen und die Referenz erhalten. In diesem Fall sollten Sie sie deklarieren__weak stattdessen .

Lange Antwort

Angenommen, Sie hatten folgenden Code:

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

Das Problem hierbei ist, dass self einen Verweis auf den Block behält; In der Zwischenzeit muss der Block einen Verweis auf self behalten, um seine Delegate-Eigenschaft abzurufen und dem Delegate eine Methode zu senden. Wenn alles andere in Ihrer App den Verweis auf dieses Objekt freigibt, ist die Anzahl der Beibehaltungen nicht Null (weil der Block darauf zeigt) und der Block macht nichts falsch (weil das Objekt darauf zeigt) und so weiter Das Objektpaar wird in den Heap gelangen und Speicher belegen, ist jedoch ohne Debugger für immer nicht erreichbar. Wirklich tragisch.

Dieser Fall könnte einfach dadurch behoben werden:

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

In diesem Code behält self den Block bei, der Block behält den Delegaten und es gibt keine Zyklen (von hier aus sichtbar; der Delegat behält möglicherweise unser Objekt, aber das liegt momentan nicht in unserer Hand). Dieser Code riskiert kein Leck auf die gleiche Weise, da der Wert der Delegate-Eigenschaft beim Erstellen des Blocks erfasst wird, anstatt bei der Ausführung nachgeschlagen zu werden. Ein Nebeneffekt ist, dass der Block beim Ändern des Delegaten nach dem Erstellen dieses Blocks weiterhin Aktualisierungsnachrichten an den alten Delegaten sendet. Ob dies wahrscheinlich ist oder nicht, hängt von Ihrer Anwendung ab.

Selbst wenn Sie mit diesem Verhalten cool waren, können Sie diesen Trick in Ihrem Fall immer noch nicht anwenden:

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

Hier übergeben Sie selfim Methodenaufruf direkt an den Delegaten, sodass Sie ihn irgendwo dort abrufen müssen. Wenn Sie die Definition des Blocktyps steuern können, ist es am besten, den Delegaten als Parameter an den Block zu übergeben:

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

Diese Lösung vermeidet den Aufbewahrungszyklus und ruft immer den aktuellen Delegaten an.

Wenn Sie den Block nicht ändern können, können Sie damit umgehen . Der Grund, warum ein Aufbewahrungszyklus eine Warnung und kein Fehler ist, ist, dass sie nicht unbedingt das Schicksal Ihrer Anwendung bedeuten. Wenn MyDataProcessordie Blöcke nach Abschluss des Vorgangs freigegeben werden können, bevor der übergeordnete Vorgang versucht, sie freizugeben, wird der Zyklus unterbrochen und alles wird ordnungsgemäß bereinigt. Wenn Sie sich dessen sicher sein könnten, wäre es das Richtige, a zu verwenden #pragma, um die Warnungen für diesen Codeblock zu unterdrücken. (Oder verwenden Sie ein Compiler-Flag pro Datei. Deaktivieren Sie die Warnung jedoch nicht für das gesamte Projekt.)

Sie können auch einen ähnlichen Trick wie oben verwenden, indem Sie eine Referenz als schwach oder nicht beibehalten deklarieren und diese im Block verwenden. Beispielsweise:

__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];
}

Alle drei oben genannten Punkte geben Ihnen eine Referenz, ohne das Ergebnis beizubehalten, obwohl sie sich alle etwas anders verhalten: __weakVersuchen Sie, die Referenz auf Null zu setzen, wenn das Objekt freigegeben wird. __unsafe_unretainedwird Sie mit einem ungültigen Zeiger verlassen; __blockfügt tatsächlich eine weitere Indirektionsebene hinzu und ermöglicht es Ihnen, den Wert der Referenz innerhalb des Blocks zu ändern (in diesem Fall irrelevant, dadp er nirgendwo anders verwendet wird).

Was am besten ist, hängt davon ab, welchen Code Sie ändern können und welchen nicht. Aber hoffentlich hat Ihnen dies einige Ideen gegeben, wie Sie vorgehen sollen.

Benzado
quelle
1
Super Antwort! Danke, ich habe ein viel besseres Verständnis dafür, was los ist und wie das alles funktioniert. In diesem Fall habe ich die Kontrolle über alles, sodass ich einige der Objekte nach Bedarf neu entwerfen kann.
XJones
18
O_O Ich bin gerade mit einem etwas anderen Problem vorbeigekommen, habe mich beim Lesen festgefahren und verlasse diese Seite jetzt mit einem sachkundigen und coolen Gefühl. Vielen Dank!
Orc JMR
Ist es richtig, dass, wenn aus irgendeinem Grund zum Zeitpunkt der Blockausführung dpdie Freigabe freigegeben wird (z. B. wenn es sich um einen Ansichtscontroller handelte und dieser eingeblendet wurde), die Zeile [dp.delegate ...EXC_BADACCESS verursacht?
Peetonn
Sollte die Eigenschaft, die den Block enthält (z. B. dataProcess.progress), sein strongoder weak?
DJ Skinner
1
Vielleicht werfen Sie einen Blick auf libextobjc , das zwei praktische Makros enthält, die aufgerufen werden @weakify(..)und @strongify(...)die es Ihnen ermöglichen, selfin Block ohne Beibehaltung zu verwenden.
25

Es gibt auch die Möglichkeit, die Warnung zu unterdrücken, wenn Sie sicher sind, dass der Zyklus in Zukunft unterbrochen wird:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"

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

#pragma clang diagnostic pop

Auf diese Weise können zum Affen nicht herum haben mit __weak, selfAliasing und explizite Ivar Vorfixierung.

Zoul
quelle
8
Klingt nach einer sehr schlechten Praxis, die mehr als 3 Codezeilen benötigt, die durch __weak id schwachSelf = self ersetzt werden können.
Ben Sinclair
3
Es gibt oft einen größeren Codeblock, der von den unterdrückten Warnungen profitieren kann.
Zoul
2
Nur dass sich __weak id weakSelf = self;das grundlegend anders verhält als das Unterdrücken der Warnung. Die Frage begann mit "... wenn Sie sicher sind, dass der Aufbewahrungszyklus unterbrochen wird"
Tim
Zu oft machen Menschen Variablen blind schwach, ohne die Auswirkungen wirklich zu verstehen. Ich habe zum Beispiel gesehen, wie Leute ein Objekt geschwächt haben und dann in dem Block, den sie tun: [array addObject:weakObject];Wenn das schwache Objekt freigegeben wurde, verursacht dies einen Absturz. Dies ist natürlich einem Haltezyklus nicht vorzuziehen. Sie müssen verstehen, ob Ihr Block tatsächlich lange genug lebt, um eine Schwächung zu rechtfertigen, und ob die Aktion im Block davon abhängen soll, ob das schwache Objekt noch gültig ist.
Mahboudz
14

Für eine allgemeine Lösung habe ich diese im vorkompilierten Header definiert. Vermeidet das Erfassen und aktiviert dennoch die Compiler-Hilfe, indem die Verwendung vermieden wirdid

#define BlockWeakObject(o) __typeof(o) __weak
#define BlockWeakSelf BlockWeakObject(self)

Dann können Sie im Code Folgendes tun:

BlockWeakSelf weakSelf = self;
self.dataProcessor.completion = ^{
    [weakSelf.delegate myAPIDidFinish:weakSelf];
    weakSelf.dataProcessor = nil;
};
Damien Pontifex
quelle
Einverstanden, könnte dies ein Problem innerhalb des Blocks verursachen. ReactiveCocoa hat eine weitere interessante Lösung für dieses Problem, mit der Sie die Verwendung selfin Ihrem Block @weakify (self) fortsetzen können. id block = ^ {@strongify (self); [self.delegate myAPIDidFinish: self]; };
Damien Pontifex
@dmpontifex es ist ein Makro von libextobjc github.com/jspahrsummers/libextobjc
Elechtron
11

Ich glaube, die Lösung ohne ARC funktioniert auch mit ARC unter Verwendung des __blockSchlüsselworts:

BEARBEITEN: Gemäß den Versionshinweisen zur Umstellung auf ARC__block bleibt ein mit Speicher deklariertes Objekt weiterhin erhalten. Verwenden Sie __weak(bevorzugt) oder __unsafe_unretained(aus Gründen der Abwärtskompatibilität).

// code sample
self.delegate = aDelegate;

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

// Use this inside blocks
__block id myself = self;

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

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

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];
Tony
quelle
Wusste nicht, dass das __blockSchlüsselwort es vermieden hat, seinen Referenten beizubehalten. Vielen Dank! Ich habe meine monolithische Antwort aktualisiert. :-)
Benzado
3
Laut Apple-Dokumenten "Im manuellen Referenzzählmodus behält __block id x; x nicht bei. Im ARC-Modus behält __block id x standardmäßig x bei (genau wie alle anderen Werte)."
XJones
11

Wenn ich ein paar andere Antworten kombiniere, verwende ich dies jetzt für ein typisiertes schwaches Selbst, um es in Blöcken zu verwenden:

__typeof(self) __weak welf = self;

Ich habe das als XCode-Code-Snippet mit dem Abschlusspräfix "welf" in Methoden / Funktionen festgelegt, das nur nach Eingabe von "we" angezeigt wird.

Kendall Helmstetter Gelner
quelle
Bist du sicher? Dieser Link und die Clang-Dokumente scheinen zu glauben, dass beide verwendet werden können und sollten, um einen Verweis auf das Objekt beizubehalten, aber keinen Link, der einen Aufbewahrungszyklus
Kendall Helmstetter Gelner
Aus den Clang-Dokumenten: clang.llvm.org/docs/BlockLanguageSpec.html "In den Sprachen Objective-C und Objective-C ++ erlauben wir den __weak-Bezeichner für __block-Variablen vom Objekttyp. Wenn die Garbage Collection nicht aktiviert ist, verursacht dieses Qualifier Diese Variablen müssen beibehalten werden, ohne dass gesendete Nachrichten gespeichert werden. "
Kendall Helmstetter Gelner
6

warning => "Das Erfassen des Selbst innerhalb des Blocks führt wahrscheinlich zu einem Haltezyklus."

Wenn Sie sich selbst oder seine Eigenschaft innerhalb eines Blocks referenzieren, der von sich selbst stark beibehalten wird, als es die obige Warnung zeigt.

Um dies zu vermeiden, müssen wir eine Woche ref

__weak typeof(self) weakSelf = self;

also anstatt zu benutzen

blockname=^{
    self.PROPERTY =something;
}

Wir sollten ... benutzen

blockname=^{
    weakSelf.PROPERTY =something;
}

Hinweis: Der Beibehaltungszyklus tritt normalerweise auf, wenn einige Objekte, die sich aufeinander beziehen und bei denen beide den Referenzzähler = 1 haben und deren Delloc-Methode nie aufgerufen wird.

Anurag Bhakuni
quelle
-1

Wenn Sie sicher sind, dass Ihr Code keinen Aufbewahrungszyklus erstellt oder dass der Zyklus später unterbrochen wird, können Sie die Warnung am einfachsten zum Schweigen bringen:

// 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];

Der Grund dafür ist, dass der Punktzugriff auf Eigenschaften bei der Xcode-Analyse berücksichtigt wird und daher

x.y.z = ^{ block that retains x}

Wenn Methodenaufrufe durch x von y (auf der linken Seite der Zuweisung) und durch y von x (auf der rechten Seite) beibehalten werden, werden Methodenaufrufe nicht derselben Analyse unterzogen, selbst wenn es sich um Methodenaufrufe mit Eigenschaftszugriff handelt Dies entspricht dem Punktzugriff, auch wenn diese Eigenschaftszugriffsmethoden vom Compiler generiert wurden

[x y].z = ^{ block that retains x}

Nur auf der rechten Seite wird eine Aufbewahrung erstellt (um y von x), und es wird keine Warnung zum Aufbewahrungszyklus generiert.

Ben Artin
quelle