wake-up-neo.com

Wie vermeide ich, mich bei der Implementierung einer API blockweise zu erfassen?

Ich habe eine funktionierende App und arbeite daran, sie in Xcode 4.2 in ARC zu konvertieren. Eine der Warnungen vor der Überprüfung besteht darin, self in einem Block festzuhalten, der 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 umgesetzt werden kann.

  • self ist eine Instanz der Klasse MyAPI
  • der folgende Code ist vereinfacht, um nur die Interaktionen mit den für meine Frage relevanten Objekten und Blöcken anzuzeigen
  • angenommen, MyAPI ruft Daten von einer Remote-Quelle ab und MyDataProcessor verarbeitet diese Daten und erstellt 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?

222
XJones

Kurze Antwort

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

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

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

Wenn Sie ARC verwenden, ändert sich die Semantik von __block und die Referenz wird beibehalten. In diesem Fall sollten Sie stattdessen __weak deklarieren.

Lange Antwort

Nehmen wir an, Sie hatten folgenden Code:

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

Das Problem hierbei ist, dass self einen Verweis auf den Block beibehält. In der Zwischenzeit muss der Block einen Verweis auf self behalten, um seine Delegate-Eigenschaft abzurufen und dem Delegaten eine Methode zu senden. Wenn alle anderen Elemente in Ihrer App den Verweis auf dieses Objekt freigeben, ist die Anzahl der Beibehaltungen nicht Null (da der Block auf dieses Objekt zeigt) und der Block macht nichts falsch (da das Objekt auf dieses Objekt zeigt) Das Objektpaar dringt in den Haufen ein und belegt Speicher, ist aber ohne Debugger für immer unerreichbar. Wirklich tragisch.

Dieser Fall könnte stattdessen einfach 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 bei und es gibt keine Zyklen (von hier aus sichtbar; der Delegat behält möglicherweise unser Objekt, aber das liegt derzeit nicht in unseren Händen). Dieser Code riskiert auf die gleiche Weise kein Leck, da der Wert der Delegate-Eigenschaft erfasst wird, wenn der Block erstellt wird, und nicht nachgeschlagen, wenn er ausgeführt wird. Ein Nebeneffekt ist, dass der Block weiterhin Aktualisierungsnachrichten an den alten Delegaten sendet, wenn Sie den Delegaten ändern, nachdem dieser Block erstellt wurde. Ob dies wahrscheinlich ist oder nicht, hängt von Ihrer Anwendung ab.

Selbst wenn Sie mit diesem Verhalten cool wären, können Sie diesen Trick in Ihrem Fall nicht anwenden:

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

Hier übergeben Sie self direkt an den Delegaten im Methodenaufruf, sodass Sie es irgendwo dort einfügen 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 Retain-Zyklus und ruft immer den aktuellen Delegaten auf.

Wenn Sie den Block nicht ändern können, können Sie damit umgehen. Der Grund, warum ein Beibehaltungszyklus eine Warnung und kein Fehler ist, ist, dass er nicht unbedingt Doom für Ihre Anwendung bedeutet. Wenn MyDataProcessor in der Lage ist, die Blöcke nach Abschluss der Operation freizugeben, wird der Zyklus unterbrochen und alles wird ordnungsgemäß bereinigt, bevor das übergeordnete Element versucht, sie freizugeben. Wenn Sie sich dessen sicher sein könnten, wäre es das Richtige, einen #pragma zu verwenden, um die Warnungen für diesen Codeblock zu unterdrücken. (Oder verwenden Sie ein Dateicompiler-Flag. Deaktivieren Sie die Warnung jedoch nicht für das gesamte Projekt.)

Sie können auch einen ähnlichen Trick verwenden, indem Sie eine Referenz als schwach oder nicht erhalten deklarieren und diesen 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 Methoden geben Ihnen eine Referenz, ohne das Ergebnis beizubehalten, obwohl sie sich alle ein wenig anders verhalten: __weak versucht, die Referenz auf Null zu setzen, wenn das Objekt freigegeben wird; __unsafe_unretained hinterlässt einen ungültigen Zeiger. __block fü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, da dp nirgendwo anders verwendet wird).

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

509
benzado

Es gibt auch die Möglichkeit, die Warnung zu unterdrücken, wenn Sie sich 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 müssen Sie sich nicht mit __weak, self Aliasing und explizitem Ivar-Präfix herumschlagen.

25
zoul

Für eine gängige Lösung habe ich diese im Vorkompilierungsheader definiert. Vermeidet das Erfassen und aktiviert dennoch die Compiler-Hilfe, indem die Verwendung von id vermieden wird.

#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;
};
14
Damien Pontifex

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

BEARBEITEN: Gemäß Übergang zu ARC Release Notes bleibt ein Objekt, das mit __block Als Speicher deklariert wurde, 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];
11
Tony

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

__typeof(self) __weak welf = self;

Ich setze das als XCode Code Snippet mit einem Vervollständigungspräfix von "welf" in Methoden/Funktionen, das nur nach der Eingabe von "we" auftaucht.

warning => "Das Erfassen von sich selbst im Block führt wahrscheinlich zu einem Retain-Zyklus."

wenn Sie auf self oder dessen Eigenschaft in einem Block verweisen, der von self stark beibehalten wird, wird dies über der Warnung angezeigt.

um dies zu vermeiden, müssen wir eine Woche ref machen

__weak typeof(self) weakSelf = self;

also anstatt zu benutzen

blockname=^{
    self.PROPERTY =something;
}

wir sollten ... benutzen

blockname=^{
    weakSelf.PROPERTY =something;
}

anmerkung: Der Beibehaltungszyklus tritt normalerweise auf, wenn ein Objekt, bei dem beide den Referenzzähler = 1 haben und dessen Delloc-Methode niemals aufgerufen wird, wie zwei Objekte aufeinander verweisen.

6
Anurag Bhakuni

Die neue Methode hierfür ist die Verwendung von @weakify und @strongify marco

@weakify(self);
[self methodThatTakesABlock:^ {
    @strongify(self);
    [self doSomething];
}];

Weitere Informationen zu @Weakify @Strongify Marco

1
Jun Jie Gan