Come evitare l'acquisizione di sé in blocchi durante l'implementazione di un'API?


222

Ho un'app funzionante e sto lavorando per convertirla in ARC in Xcode 4.2. Uno degli avvertimenti di pre-controllo prevede l'acquisizione selfforte in un blocco che porta a un ciclo di mantenimento. Ho fatto un semplice esempio di codice per illustrare il problema. Credo di capire cosa significhi, ma non sono sicuro del modo "corretto" o raccomandato di implementare questo tipo di scenario.

  • self è un'istanza della classe MyAPI
  • il codice seguente è semplificato per mostrare solo le interazioni con gli oggetti e i blocchi rilevanti per la mia domanda
  • supponiamo che MyAPI ottenga i dati da un'origine remota e MyDataProcessor funzioni su tali dati e produca un output
  • il processore è configurato con blocchi per comunicare avanzamento e stato

esempio di codice:

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

Domanda: cosa sto facendo "sbagliato" e / o come dovrebbe essere modificato per conformarsi alle convenzioni ARC?

Risposte:


509

Risposta breve

Invece di accedere selfdirettamente, dovresti accedervi indirettamente, da un riferimento che non verrà mantenuto. Se non stai usando il conteggio di riferimento automatico (ARC) , puoi farlo:

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

La __blockparola chiave indica le variabili che possono essere modificate all'interno del blocco (non lo stiamo facendo) ma non vengono automaticamente conservate quando il blocco viene mantenuto (a meno che non si stia utilizzando ARC). Se lo fai, devi essere sicuro che nient'altro tenterà di eseguire il blocco dopo il rilascio dell'istanza MyDataProcessor. (Data la struttura del tuo codice, questo non dovrebbe essere un problema.) Altre informazioni__block .

Se si utilizza ARC , la semantica delle __blockmodifiche e il riferimento verranno mantenuti, nel qual caso è necessario dichiararlo__weak .

Risposta lunga

Supponiamo che tu abbia un codice come questo:

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

Il problema qui è che l'io sta mantenendo un riferimento al blocco; nel frattempo il blocco deve conservare un riferimento a self per recuperare la sua proprietà delegata e inviare un metodo al delegato. Se tutto il resto nella tua app rilascia il suo riferimento a questo oggetto, il suo conteggio di mantenimento non sarà zero (perché il blocco è puntato su di esso) e il blocco non sta facendo nulla di sbagliato (perché l'oggetto sta puntando su di esso) e così la coppia di oggetti colerà nell'heap, occupando memoria ma per sempre irraggiungibile senza un debugger. Tragico, davvero.

Quel caso potrebbe essere facilmente risolto facendo questo invece:

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

In questo codice, self mantiene il blocco, il blocco mantiene il delegato e non ci sono cicli (visibili da qui; il delegato può conservare il nostro oggetto ma è fuori dalle nostre mani in questo momento). Questo codice non rischia una perdita allo stesso modo, poiché il valore della proprietà del delegato viene acquisito durante la creazione del blocco, anziché essere cercato quando viene eseguito. Un effetto collaterale è che, se si modifica il delegato dopo la creazione di questo blocco, il blocco invierà comunque messaggi di aggiornamento al vecchio delegato. La probabilità che ciò accada o meno dipende dalla tua applicazione.

Anche se sei stato bravo con quel comportamento, non puoi ancora usare quel trucco nel tuo caso:

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

Qui stai passando selfdirettamente al delegato nella chiamata del metodo, quindi devi farlo da qualche parte. Se si ha il controllo sulla definizione del tipo di blocco, la cosa migliore sarebbe passare il delegato nel blocco come parametro:

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

Questa soluzione evita il ciclo di conservazione e chiama sempre il delegato corrente.

Se non riesci a modificare il blocco, puoi gestirlo . Il motivo per cui un ciclo di mantenimento è un avvertimento, non un errore, è che non comportano necessariamente un destino per la tua applicazione. Se MyDataProcessorè in grado di rilasciare i blocchi al termine dell'operazione, prima che il genitore provi a rilasciarlo, il ciclo verrà interrotto e tutto verrà ripulito correttamente. Se puoi esserne sicuro, la cosa giusta da fare sarebbe usare a #pragmaper sopprimere gli avvisi per quel blocco di codice. (O utilizzare un flag di compilatore per file. Ma non disabilitare l'avviso per l'intero progetto.)

Puoi anche cercare di usare un trucco simile sopra, dichiarando un riferimento debole o non trattenuto e usandolo nel blocco. Per esempio:

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

Tutti e tre i precedenti ti daranno un riferimento senza conservare il risultato, sebbene si comportino tutti in modo leggermente diverso: __weakproveranno ad azzerare il riferimento quando l'oggetto viene rilasciato; __unsafe_unretainedti lascerà con un puntatore non valido; __blockaggiungerà effettivamente un altro livello di riferimento indiretto e ti consentirà di modificare il valore del riferimento dall'interno del blocco (irrilevante in questo caso, poiché dpnon viene utilizzato da nessun'altra parte).

La cosa migliore dipenderà dal codice che è possibile modificare e da ciò che non è possibile. Ma spero che questo ti abbia dato alcune idee su come procedere.


1
Risposta fantastica! Grazie, ho una comprensione molto migliore di ciò che sta succedendo e di come tutto funzioni. In questo caso, ho il controllo su tutto, quindi riprogetto alcuni degli oggetti secondo necessità.
XJones,

18
O_O Stavo passando di lì con un problema leggermente diverso, mi sono bloccato a leggere e ora ho lasciato questa pagina sentendomi ben informata e simpatica. Grazie!
Orco JMR

è corretto, che se per qualche motivo al momento dell'esecuzione del blocco dpverrà rilasciato (ad esempio se fosse un controller di visualizzazione e fosse poped), allora la riga [dp.delegate ...causerà EXC_BADACCESS?
Peetonn,

La proprietà che contiene il blocco (ad es. DataProcess.progress) dovrebbe essere strongo weak?
djskinner,

1
Potresti dare un'occhiata a libextobjc che fornisce due utili macro chiamate @weakify(..)e @strongify(...)che ti consente di utilizzare selfin blocco in modo non-ritentivo.

25

C'è anche la possibilità di sopprimere l'avviso quando sei sicuro che il ciclo si interromperà in futuro:

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

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

#pragma clang diagnostic pop

In questo modo non devi preoccuparti __weak, selfaliasing e prefisso esplicito ivar.


8
Sembra una brutta pratica che richiede più di 3 righe di codice che possono essere sostituite con __weak id weakSelf = self;
Ben Sinclair,

3
C'è spesso un blocco di codice più grande che può beneficiare degli avvisi soppressi.
Zoul,

2
Tranne che __weak id weakSelf = self;ha un comportamento fondamentalmente diverso rispetto alla soppressione dell'avvertimento. La domanda è iniziata con "... se sei sicuro che il ciclo di mantenimento si interromperà"
Tim

Troppo spesso le persone rendono le variabili alla cieca deboli, senza veramente capire le conseguenze. Ad esempio, ho visto le persone indebolire un oggetto e poi, nel blocco, lo fanno: [array addObject:weakObject];se il deboleObject è stato rilasciato, ciò provoca un arresto anomalo. Chiaramente ciò non è preferito su un ciclo di mantenimento. Devi capire se il tuo blocco vive effettivamente abbastanza a lungo da giustificare l'indebolimento, e anche se vuoi che l'azione nel blocco dipenda dal fatto che l'oggetto debole sia ancora valido.
Mahboudz,

14

Per una soluzione comune, li ho definiti nell'intestazione precompilata. Evita l'acquisizione e abilita ancora l'aiuto del compilatore evitando di usarloid

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

Quindi nel codice puoi fare:

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

D'accordo, ciò potrebbe causare un problema all'interno del blocco. ReactiveCocoa ha un'altra soluzione interessante per questo problema che ti consente di continuare a usare selfall'interno del tuo blocco @weakify (self); id block = ^ {@strongify (self); [self.delegate myAPIDidFinish: self]; };
Damien Pontifex,

@dmpontifex è una macro di libextobjc github.com/jspahrsummers/libextobjc
Elechtron

11

Credo che la soluzione senza ARC funzioni anche con ARC, usando la __blockparola chiave:

EDIT: in base alle note sulla versione Transitioning to ARC , un oggetto dichiarato con __blockmemoria è ancora conservato. Utilizzare __weak(preferito) o __unsafe_unretained(per compatibilità con le versioni precedenti).

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

Non ho capito che la __blockparola chiave evitava di conservare il suo referente. Grazie! Ho aggiornato la mia risposta monolitica. :-)
benzado,

3
Secondo i documenti Apple "Nella modalità di conteggio dei riferimenti manuali, __block id x; ha l'effetto di non conservare x. In modalità ARC, __block id x; il valore predefinito è il mantenimento di x (proprio come tutti gli altri valori)."
XJones,

11

Combinando alcune altre risposte, questo è quello che uso ora per un sé debole tipizzato da usare nei blocchi:

__typeof(self) __weak welf = self;

L'ho impostato come Snippet di codice XCode con un prefisso di completamento "welf" in metodi / funzioni, che colpisce dopo aver digitato solo "noi".


Sei sicuro? Questo link e i documenti di clang sembrano pensare che entrambi possano e debbano essere usati per mantenere un riferimento all'oggetto ma non un link che causerà un ciclo di mantenimento: stackoverflow.com/questions/19227982/using-block-and-weak
Kendall Helmstetter Gelner,

Dai documenti clang: clang.llvm.org/docs/BlockLanguageSpec.html "Nei linguaggi Objective-C e Objective-C ++, consentiamo l'identificatore __weak per __block variabili di tipo oggetto. Se la garbage collection non è abilitata, questo qualificatore causa queste variabili devono essere conservate senza conservare i messaggi inviati. "
Kendall Helmstetter Gelner,


6

warning => "L'acquisizione di sé all'interno del blocco probabilmente conduce un ciclo di mantenimento"

quando ti riferisci a te stesso o alla sua proprietà all'interno di un blocco che è fortemente trattenuto da te stesso rispetto a quanto mostrato sopra.

quindi per evitarlo dobbiamo farlo una settimana rif

__weak typeof(self) weakSelf = self;

quindi invece di usare

blockname=^{
    self.PROPERTY =something;
}

dovremmo usare

blockname=^{
    weakSelf.PROPERTY =something;
}

nota: il ciclo di mantenimento si verifica in genere quando alcuni oggetti due che si riferiscono l'uno all'altro mediante i quali entrambi hanno un riferimento count = 1 e il loro metodo delloc non vengono mai chiamati.



-1

Se sei sicuro che il tuo codice non creerà un ciclo di conservazione o che il ciclo verrà interrotto in seguito, il modo più semplice per silenziare l'avviso è:

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

Il motivo per cui funziona è che mentre l'accesso al punto delle proprietà viene preso in considerazione dall'analisi di Xcode, e quindi

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

è visto come trattenuto da x di y (sul lato sinistro dell'assegnazione) e da y di x (sul lato destro), le chiamate al metodo non sono soggette alla stessa analisi, anche quando sono chiamate al metodo con accesso alla proprietà equivalenti all'accesso punto, anche quando quei metodi di accesso alle proprietà sono generati dal compilatore, quindi in

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

solo il lato destro viene visto come creazione di un mantenimento (per y di x) e non viene generato alcun avviso del ciclo di mantenimento.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.