2012-05-03 12 views
13

Quando si scrive un metodo che accetta un blocco come argomento, è necessario fare qualcosa di speciale come copiare il blocco nell'heap prima di eseguirlo? Per esempio, se ho avuto il seguente metodo:Passaggio di blocchi in Objective-C

- (void)testWithBlock:(void (^)(NSString *))block { 
    NSString *testString = @"Test"; 
    block(testString); 
} 

Devo fare qualcosa con block prima di chiamare, o quando entra il metodo? O è sopra il modo corretto di utilizzare il blocco passato? Inoltre, è il modo seguente di chiamare il metodo corretto, o dovrei fare qualcosa con il blocco prima di passarlo?

[object testWithBlock:^(NSString *test){ 
    NSLog(@"[%@]", test); 
}]; 

Dove fare ho bisogno di copiare il blocco? E come sarebbe stato diverso se non stavo usando ARC?

+0

Stai usando ARC? –

+0

@pcperini, si. – rid

risposta

18

Quando si riceve un blocco come parametro del metodo, quel blocco potrebbe essere l'originale che era stato creato nello stack o che poteva essere copiato (un blocco sull'heap). Per quanto ne so, non c'è modo di dirlo. Quindi la regola generale è che se si esegue il blocco all'interno del metodo che lo sta ricevendo, non è necessario copiarlo. Se si intende passare quel blocco a un altro metodo (che può essere o non essere eseguito immediatamente), non è necessario copiarlo (il metodo di ricezione dovrebbe copiarlo se intende mantenerlo in giro). Tuttavia, se si intende archiviare il blocco in qualsiasi modo per un posto per l'esecuzione successiva, è necessario copiarlo.L'esempio principale molte persone utilizzano sia una sorta di blocco completamento detenuti come una variabile di istanza:

typedef void (^IDBlock) (id); 
@implementation MyClass{ 
    IDBlock _completionBlock; 
} 

Tuttavia, è anche necessario copiare se avete intenzione di aggiungerlo a qualsiasi tipo di classe di raccolta, come un NSArray o NSDictionary. In caso contrario, riceverai degli errori (molto probabilmente EXC_BAD_ACCESS) o forse un danneggiamento dei dati quando proverai a eseguire il blocco in un secondo momento.

Quando si esegue un blocco, è importante verificare prima se il blocco è nil. Objective-c consente di passare nil a un parametro del metodo di blocco. Se quel blocco è nullo, otterrai EXC_BAD_ACCESS quando provi ad eseguirlo. Fortunatamente questo è facile da fare. Nel tuo esempio, scrivi:

- (void)testWithBlock:(void (^)(NSString *))block { 
    NSString *testString = @"Test"; 
    if (block) block(testString); 
} 

Ci sono considerazioni sulle prestazioni nella copia dei blocchi. Rispetto alla creazione di un blocco sullo stack, copiare un blocco sull'heap non è banale. Non è un grosso problema in generale, ma se si utilizza un blocco in modo iterativo o si utilizza un gruppo di blocchi in modo iterativo e si copiano su ogni esecuzione, si creerà un impatto sulle prestazioni. Quindi, se il tuo metodo - (void)testWithBlock:(void (^)(NSString *))block; era in un qualche tipo di ciclo, la copia di quel blocco potrebbe danneggiare le tue prestazioni se non hai bisogno di copiarlo.

Un altro posto in cui è necessario copiare un blocco è se si intende richiamare quel blocco di per sé (ricorsione del blocco). Questo non è tutto ciò che è comune, ma è necessario copiare il blocco se si intende farlo. Vedere la mia domanda/risposta su SO qui: Recursive Blocks In Objective-C.

Infine, se si intende memorizzare un blocco, è necessario prestare molta attenzione alla creazione di cicli di conservazione. I blocchi manterranno qualsiasi oggetto passato in esso, e se tale oggetto è una variabile di istanza, manterrà la classe della variabile di istanza (self). Personalmente amo i blocchi e li uso sempre. Ma c'è un motivo per cui Apple non usa/memorizza blocchi per le loro classi UIKit e invece si attacca a un modello di destinazione/azione o delegato. Se tu (la classe che crea il blocco) stai mantenendo la classe che sta ricevendo/copiando/memorizzando il blocco, e in quel blocco fai riferimento a te stesso oa QUALSIASI variabile di istanza di classe, hai creato un ciclo di conservazione (classA -> classB - > blocco -> classeA). Questo è straordinariamente facile da fare, ed è qualcosa che ho fatto troppe volte. Inoltre, "Leaks" in Instruments non lo cattura. Il metodo per aggirare questo problema è semplice: è sufficiente creare una variabile temporanea __weak variabile (per ARC) o __block (non ARC) e il blocco non conserverà tale variabile. Così, per esempio, il seguente sarebbe un conservano ciclo se le copie 'oggetto'/memorizza il blocco:

[object testWithBlock:^(NSString *test){ 
    _iVar = test; 
    NSLog(@"[%@]", test); 
}]; 

Tuttavia, per risolvere questo (utilizzando ARC):

__weak IVarClass *iVar = _iVar; 
[object testWithBlock:^(NSString *test){ 
    iVar = test; 
    NSLog(@"[%@]", test); 
}]; 

È inoltre possibile fare qualcosa di simile:

__weak ClassOfSelf _self = self; 
[object testWithBlock:^(NSString *test){ 
    _self->_iVar = test; 
    NSLog(@"[%@]", test); 
}]; 

Nota che molte persone non piace quanto sopra perché ritengono che sia fragile, ma è un modo valido di accesso variabili. Aggiornamento - Il compilatore corrente ora avverte se si tenta di accedere direttamente a una variabile usando '->'. Per questo motivo (oltre a motivi di sicurezza) è meglio creare una proprietà per le variabili a cui si desidera accedere. Quindi invece di _self->_iVar = test; dovresti usare: _self.iVar = test;.

UPDATE (+ info)

In generale, è meglio prendere in considerazione il metodo che riceve il blocco, come responsabile per determinare se il blocco deve essere copiato piuttosto che il chiamante. Questo perché il metodo di ricezione può essere l'unico in grado di sapere per quanto tempo il blocco deve essere mantenuto attivo o se deve essere copiato. Tu (come il programmatore) ovviamente saprai questa informazione quando scrivi la chiamata, ma se consideri mentalmente il chiamante e il ricevente in oggetti separati, il chiamante dà al ricevitore il blocco e lo ha fatto. Pertanto, non dovrebbe essere necessario sapere cosa viene fatto con il blocco dopo che è sparito. D'altra parte, è possibile che il chiamante abbia già copiato il blocco (forse ha memorizzato il blocco e lo sta ora consegnando ad un altro metodo) ma il ricevitore (che intende anche archiviare il blocco) dovrebbe comunque copiare il blocco (anche se il blocco è già stato copiato). Il ricevitore non può sapere che il blocco è già stato copiato e alcuni blocchi che riceve possono essere copiati mentre altri potrebbero non esserlo. Quindi il ricevitore dovrebbe sempre copiare un blocco che intende mantenere in giro? Ha senso? Questo in sostanza è una buona pratica di progettazione orientata agli oggetti. Fondamentalmente, chi ha le informazioni è responsabile per gestirle.

I blocchi sono ampiamente utilizzati nel GCD di Apple (Grand Central Dispatch) per abilitare facilmente il multi-threading. In generale, non è necessario copiare un blocco quando lo si invia su GCD. Stranamente, questo è leggermente contro-intuitivo (se ci pensate) perché se si invia un blocco in modo asincrono, spesso il metodo in cui il blocco è stato creato ritornerà prima dell'esecuzione del blocco, che in genere significherebbe che il blocco scadrà poiché è un oggetto stack. Non penso che GCD copi il blocco nello stack (l'ho letto da qualche parte ma non sono riuscito a trovarlo di nuovo), invece penso che la vita del thread venga estesa inserendo un altro thread.

Mike Ash dispone di ampi articoli su blocchi, GCD e ARC, che si possono trovare utili:

+0

Che ne dici di un metodo che accetta un gestore di completamento che viene eseguito in seguito? Il chiamante o il metodo dovrebbe copiare il blocco? – rid

+0

Il metodo dovrebbe copiare il blocco. Il chiamante non dovrebbe sapere come viene trattato il blocco.Se metti la responsabilità sul chiamante, e il metodo cambia la sua implementazione (decide di archiviare il blocco invece di eseguirlo immediatamente) dovrai trovare tutti i chiamanti di quel metodo e farli copiare. Non è divertente. Il metodo saprà meglio del chiamante se è necessario copiare il blocco. Sfortunatamente, è responsabilità del chiamante assicurarsi che non crei un ciclo di conservazione, che credo sia il motivo per cui Apple non memorizza quasi mai blocchi dalle loro API pubbliche. –

+0

@Radu Ho aggiornato la mia risposta per aggiungere ulteriori informazioni. Ho anche incluso alcuni link ad articoli che parlano di blocchi più approfonditi di quanto sia possibile qui. –

7

Tutto sembra a posto. Anche se, si potrebbe desiderare di controllare due volte il parametro di blocco:

@property id myObject; 
@property (copy) void (^myBlock)(NSString *); 

....

- (void)testWithBlock: (void (^)(NSString *))block 
{ 
    NSString *testString = @"Test"; 
    if (block) 
    { 
     block(test); 
     myObject = Block_copy(block); 
     myBlock = block; 
    } 
} 

...

[object testWithBlock: ^(NSString *test) 
{ 
    NSLog(@"[%@]", test); 
}]; 

dovrebbe andare bene. E credo che stiano anche cercando di eliminare gradualmente lo Block_copy(), ma non lo hanno ancora fatto.

+0

Quindi, se non dovessi usare ARC, dovrei fare una copia del blocco quando inserisco il metodo, ma ARC lo fa automaticamente? O sarebbe simile anche se non usassi ARC? – rid

+2

Hai solo bisogno di copiare il blocco se lo stai mantenendo al di là dell'esecuzione del metodo corrente. ARC lo copierà per te se lo memorizzi in una variabile di blocco forte non locale. Se lo memorizzi come un 'id', allora ARC lo manterrebbe semplicemente, il che non è abbastanza buono. –

+0

@KenThomases, non sono sicuro di aver capito completamente. Puoi per favore postare un esempio di dove avrei bisogno di copiarlo? – rid

4

Come i blocchi di programmazione guida argomenti dice in 'Copying Blocks':

In genere, non dovrebbe essere necessario copiare (o mantenere) un blocco. Hai solo bisogno di fare una copia quando ti aspetti che il blocco venga utilizzato dopo la distruzione dell'ambito entro il quale è stato dichiarato.

Nel caso si sta descrivendo, si può sostanzialmente pensare al blocco come semplicemente essere un parametro al metodo, proprio come se si trattasse di un tipo primitivo int o altro. Quando il metodo viene chiamato, lo spazio nello stack sarà allocato per i parametri del metodo e quindi il blocco vivrà in pila durante l'intera esecuzione del metodo (proprio come tutti gli altri parametri). Quando il frame dello stack viene estratto dalla cima dello stack al ritorno del metodo, la memoria dello stack allocata al blocco verrà deallocata. Quindi, durante l'esecuzione del tuo metodo, il blocco è garantito come vivo, quindi non c'è gestione della memoria da trattare qui (in entrambi i casi ARC e non ARC). In altre parole, il tuo codice va bene. Puoi semplicemente chiamare il blocco all'interno del metodo.

Come suggerisce il testo citato, l'unica volta che è necessario copiare un blocco in modo esplicito è quando si desidera che sia accessibile dall'ambito di applicazione in cui è stato creato (nel tuo caso, oltre la durata dello stack frame di il tuo metodo). Ad esempio, si supponga di volere un metodo che recupera alcuni dati dal Web ed esegue un blocco di codice quando il recupero è completo. La vostra firma del metodo potrebbe essere simile:

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(void))completionHandler;

Poiché i dati recuperano accade in modo asincrono, si desidera mantenere il blocco intorno (probabilmente in una proprietà della classe) e quindi eseguire il blocco di una volta che i dati sono stati completamente recuperato.In questo caso, l'implementazione potrebbe essere simile:

@interface MyClass 

@property (nonatomic, copy) void(^dataCompletion)(NSData *); 

@end 



@implementation MyClass 
@synthesize dataCompletion = _dataCompletion; 

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(NSData *fetchedData))completionHandler { 
    self.dataCompletion = completionHandler; 
    [self fetchDataFromURL:url]; 
} 

- (void)fetchDataFromURL:(NSURL *)url { 
    // Data fetch starts here 
} 

- (void)finishedFetchingData:(NSData *)fetchedData { 
    // Called when the data is done being fetched 
    self.dataCompletion(fetchedData) 
    self.dataCompletion = nil; 
} 

In questo esempio, utilizzando una proprietà con un copy semantica si esibirà in un Block_copy() sul blocco e copiarlo al mucchio. Questo succede nella riga self.dataCompletion = completionHandler. Pertanto, il blocco viene spostato dal frame stack del metodo -getDataFromURL:completionHandler: all'heap che consente di chiamarlo successivamente nel metodo finishedFetchingData:. Nel secondo metodo, la riga self.dataCompletion = nil annulla la proprietà e invia un Block_release() al blocco memorizzato, quindi lo rilascia.

utilizzando una proprietà in questo modo è bello dato che sarà essenzialmente di gestire tutta la gestione della memoria del blocco per voi (basta assicurarsi che sia una proprietà copy (o strong) e non semplicemente un retain) e funziona in entrambi i non- Casi ARC e ARC. Se invece volessi utilizzare una variabile di istanza non elaborata per archiviare il blocco e lavorassi in un ambiente non ARC, dovresti chiamare Block_copy(), Block_retain() e Block_release() in tutti i posti appropriati se vuoi mantenere il blocco attorno a qualsiasi più lungo della durata del metodo in cui è passato come parametro. Lo stesso codice di cui sopra scritto utilizzando un Ivar al posto di una proprietà sarebbe simile a questa:

@interface MyClass { 
    void(^dataCompletion)(NSData *); 
} 

@end 



@implementation MyClass 

- (void)getDataFromURL:(NSURL *)url completionHandler:(void(^)(NSData *fetchedData))completionHandler { 
    dataCompletion = Block_copy(completionHandler); 
    [self fetchDataFromURL:url]; 
} 

- (void)fetchDataFromURL:(NSURL *)url { 
    // Data fetch starts here 
} 

- (void)finishedFetchingData:(NSData *)fetchedData { 
    // Called when the data is done being fetched 
    dataCompletion(fetchedData) 
    Block_release(dataCompletion); 
    dataCompletion = nil; 
} 
+0

puoi semplicemente usare '[completionHandler copy]' invece di 'Block_copy (completionHandler)' e '[dataCompletion release]' invece di 'Block_release (dataCompletion)' – user102008

0

Sai che ci sono due tipi di blocchi:

  1. blocchi memorizzati nello stack, quelli che si scrivi esplicitamente come^{...}, e questo scompare non appena la funzione viene creata nei ritorni, proprio come normali variabili dello stack. Quando si chiama un blocco stack dopo che la funzione a cui apparteneva è tornata, accadono cose brutte.

  2. blocchi nell'heap, quelli che si ottengono quando si copia un altro blocco, quelli che vivono finché un altro oggetto mantiene un riferimento a essi, proprio come gli oggetti normali.

L'unico motivo per voi di copiare un blocco è quando si è dato un blocco che è, o può essere un blocco pila (esplicito blocco locale^{...}, o un metodo argomento la cui origine si don lo so), E che tu vuoi estendere la sua durata fuori dal limitato dei blocchi dello stack, E che il compilatore non fa già quel lavoro per te.

Pensa: mantenere un blocco in una variabile di istanza.

Aggiunta di un blocco in una raccolta come NSArray.

Questi sono esempi comuni di casi in cui è necessario copiare un blocco quando non si è certi che sia già un blocco heap.

Nota che il compilatore lo fa per te quando un blocco viene chiamato in un altro blocco.