2015-06-11 13 views
12

Sto lavorando con le classi di richieste di rete e sono preoccupato per gli arresti . Ad esempio, lavorando con chiusure è molto facile come si passa un metodo di callback per una funzione:Closures vs Delegate pattern

// some network client 
func executeHttpRequest(#callback: (success: Bool) -> Void) { 
    // http request 

    callback(true) 
} 

// View Controller 
func reload() { 
    networkClient.executeHttpRequest() { (success) -> Void in 
     self.myLabel.text = "it succeeded" // NOTE THIS CALL 
    } 
} 

Tuttavia, poiché il processo che deve eseguire il callback è asincrona, quando callback interagiscono con elemento classe contenitore (in questo caso un UIKit classe) può essere vulnerabile a crash in situazioni come

  1. l'utente navigato a un altro controller di vista, mentre il compito asincrono era ancora in esecuzione
  2. l'utente ha premuto il pulsante di casa, mentre il compito asincrono era ancora in esecuzione
  3. Etc ...

Così, quando il callback finalmente viene licenziato, self.myLabel.text potrebbe risultare in un incidente, come il controller della vista a cui self riferivo potrebbe già essere deallocato.

Fino a questo punto. Ho ragione o implemento rapidamente qualcosa internamente in modo che non accada mai?

Se ho ragione, ecco quando lo schema dei delegati è utile, poiché le variabili delegate sono weak references, il che significa che non vengono mantenute in memoria se deallocate.

// some network client 

// NOTE this variable is an OPTIONAL and it's also a WEAK REFERENCE 
weak var delegate: NetworkClientDelegate? 

func executeHttpRequest() { 
    // http request 

    if let delegate = self.delegate { 
     delegate.callback(success: true) 
    } 
} 

Si noti come self.delegate, dal momento che è un weak reference, punterà al nil se il regolatore View (che implementa il protocollo NetworkClientDelegate) viene deallocato, e la richiamata non è chiamato in questo caso.

La mia domanda sarebbe: le chiusure hanno qualcosa di speciale che le rende una buona scelta in scenari simili a questo, piuttosto che tornare a delegare lo schema? Sarebbe bene se vengono forniti esempi di chiusure (che non finiranno in arresti anomali dovuti al puntatore nullo). Grazie.

+1

Bene, il primo non si arresta in modo anomalo, poiché il riferimento a se stesso nel blocco impedisce di essere deallocato mentre il blocco è ancora attivo. – dan

+2

Piccolo suggerimento Swift: usa 'delegate? .callback (success: true)' invece di 'se let delegate = self.delegate {delegate.callback (success: true)}'. Inoltre non avresti bisogno del 'self' per accedere a una proprietà;) – Kametrixom

risposta

14

Quindi, quando il callback viene finalmente attivato, self.myLabel.il testo potrebbe causare un arresto anomalo, poiché il controller di visualizzazione a cui si riferiva autonomamente poteva già essere deallocato.

Se self è stata importata nella chiusura come un riferimento forte, è garantito che saranno selfnon disallocazione fino alla chiusura è stato terminato l'esecuzione. Cioè, il controller della vista è ancora attivo quando viene chiamata la chiusura, anche se la vista non è visibile in questo momento. L'istruzione self.myLabel.text = "it succeeded" verrà eseguita, ma anche l'etichetta non sarà visibile, non si bloccherà.

C'è, però, un problema sottile, che può portare a un crash in determinate circostanze:

Supponiamo, la chiusura ha l'ultimo e unico forte riferimento al controller della vista. La chiusura termina e successivamente viene deallocato, che rilascia anche l'ultimo riferimento forte al controller della vista. Questo inevitabile chiamerà il metodo dealloc del controller di visualizzazione. Il metodo dealloc verrà eseguito sullo stesso thread in cui verrà eseguita la chiusura. Ora che il controller della vista è un oggetto UIKit, DEVE essere garantito che tutti i metodi inviati a questo oggetto verranno eseguiti sulla filettatura principale . Quindi, IFF dealloc verrà effettivamente eseguito su qualche altro thread, il tuo codice potrebbe bloccarsi.

Un approccio adeguato richiederebbe di "annullare" un'operazione asincrona il cui risultato non è più necessario dal controllore della vista quando è "chiuso". Questo, ovviamente, richiede che il tuo "compito" possa essere cancellato.

per alleviare alcuni problemi con il tuo primo approccio, si potrebbe catturare un debole di riferimento del controller della vista invece di un riferimento forte quando si definisce la chiusura. Ciò non impedisce l'esecuzione dell'attività asincrona fino al suo completamento, ma nel gestore di completamento è possibile verificare se il controller di visualizzazione è ancora attivo e salvarlo se non esiste più.

E, se hai bisogno di "tenere" un oggetto UIKit in qualche chiusura che possono eseguire su alcuni thread arbitrario, fare in modo che questo potrebbe essere il forte richiamo scorso, e assicurare che questo ultimo riferimento forte viene rilasciato sulla main thread.

Consulta anche: Using weak self in dispatch_async function

Edit:

La mia domanda sarebbe: fare le chiusure hanno nulla di speciale che li rende una buona scelta in scenari simili a questo, invece di tornare a modello delegato?

direi, le chiusure sono l'approccio "migliore" in molti casi d'uso:

I delegati sono più inclini a problemi come i riferimenti circolari di chiusure (dal momento che sono "di proprietà" di un oggetto, e questo oggetto potrebbe essere catturato come variabile nel delegato).

L'uso a caso classico per la chiusura come gestori di completamento migliora anche la "località" del codice che rende più comprensibile: si stato cosa deve accadere quando un compito finito subito dopo la dichiarazione invocando il compito - non importa quanto tempo ci vorrà.

L'enorme vantaggio delle chiusure rispetto alle "funzioni" regolari è che una chiusura cattura l'intero "contesto" nel momento in cui viene definito. Cioè, può riferirsi a variabili e "importarle" nella chiusura al momento in cui è definito - e usarlo quando viene eseguito, non importa quando si verifica e quando lo "stack" originale al momento della definizione è già andato.

+0

Ho scelto la tua risposta poiché ha una spiegazione completa di come funziona internamente. –

0

Se fossi in te, userei le chiusure poiché sono più convenienti e flessibili della delega in questo scenario.

Per quanto riguarda l'utente che naviga verso altri controller di visualizzazione e l'operazione asincrona che si sta ancora eseguendo in background, è possibile mantenere un riferimento a tali operazioni e ogni volta che l'utente si allontana dal controller di visualizzazione è possibile annullarle.

In alternativa, si potrebbe verificare se vista del controller vista è visibile prima di aggiornare l'interfaccia utente:

viewController.isViewLoaded && viewController.view.window 

Per quanto riguarda l'applicazione di entrare sfondo/primo piano, si può mettere in pausa/riprendere le operazioni dal overridng il applicationDidEnterBackground e applicationWillEnterForeground o registrando per le notifiche appropriate: UIApplicationWillEnterForegroundNotification/UIApplicationDidEnterBackgroundNotification

Si consiglia vivamente di utilizzare AFNetworking poiché è API offre tutte le cose che ho citato sopra, e molto altro ancora.