10

Ho appena terminato il debug di una perdita molto fastidiosa di UIViewController, in modo tale che UIViewController non è stato deallocato anche dopo aver chiamato dismissViewControllerAnimated.Perché un riferimento forte al controllore UIViewController in performBatchUpdates perde un'attività?

ho rintracciato il problema al seguente blocco di codice:

self.dataSource.doNotAllowUpdates = YES; 

    [self.collectionView performBatchUpdates:^{ 
     [self.collectionView reloadItemsAtIndexPaths:@[indexPath]]; 
    } completion:^(BOOL finished) { 
     self.dataSource.doNotAllowUpdates = NO; 
    }]; 

In sostanza, se faccio una chiamata a performBatchUpdates e poi subito chiamare dismissViewControllerAnimated, l'UIViewController viene perdeva e il metodo di che UIViewControllerdealloc mai viene chiamato Il controllore UIView è in agguato per sempre.

Qualcuno può spiegare questo comportamento? Presumo che performBatchUpdates venga eseguito su un intervallo di tempo, ad esempio 500 ms, quindi suppongo che dopo detto intervallo, chiamerà questi metodi e quindi attiverà il dealloc.

La correzione sembra essere questo:

self.dataSource.doNotAllowUpdates = YES; 

    __weak __typeof(self)weakSelf = self; 

    [self.collectionView performBatchUpdates:^{ 
     __strong __typeof(weakSelf)strongSelf = weakSelf; 

     if (strongSelf) { 
      [strongSelf.collectionView reloadItemsAtIndexPaths:@[indexPath]]; 
     } 
    } completion:^(BOOL finished) { 
     __strong __typeof(weakSelf)strongSelf = weakSelf; 

     if (strongSelf) { 
      strongSelf.dataSource.doNotAllowUpdates = NO; 
     } 
    }]; 

nota che la variabile BOOL membro, doNotAllowUpdates, è una variabile ho aggiunto che impedisce qualsiasi tipo di aggiornamenti dataSource/CollectionView mentre una chiamata a performBatchUpdates è in esecuzione.

Ho cercato in giro per discussioni online sull'opportunità o meno di utilizzare il modello weakSelf/strongSelf in performBatchUpdates, ma non ho trovato nulla di specifico su questa domanda.

Sono felice di essere stato in grado di arrivare in fondo a questo bug, ma mi piacerebbe uno sviluppatore iOS più intelligente per spiegarmi questo comportamento che sto vedendo.

+0

Sarebbe interessante vedere se è possibile risolvere se si trattasse del blocco degli aggiornamenti o del blocco di completamento che stava causando il ciclo di conservazione. Come dici tu, nessuno dei due dovrebbe rimanere permanentemente al loro blocco: posso solo supporre che quando la vista raccolta viene rimossa dalla sua superview, smetta di eseguire qualsiasi aggiornamento in batch e non chiami o rilasci il blocco di completamento. Merita un radar secondo me. – jrturton

+0

@jrturton Ahh questa sembra la spiegazione più probabile! Vedrò se posso riproporlo nel debugger basato su questa intuizione. – esilver

+0

@jrturton ahimè non è in grado di eseguire la repro nel debugger ... sembra che entrambi i blocchi, almeno, vengano sempre chiamati. Forse gli interni non hanno nulla a che fare con i blocchi, anche se il rigore viene chiamato nel mezzo? – esilver

risposta

-1

Come si è capito, quando non si utilizza weak viene creato un ciclo di mantenimento.

Il conservano ciclo è causata da self avendo una forte riferimento collectionView e collectionView ora ha un forte riferimento self.

Si deve sempre presumere che self avrebbe potuto essere deallocato prima dell'esecuzione di un blocco asincrono. Per gestire questo in modo sicuro due cose devono essere fatte:

  1. Usare sempre un debole riferimento alla self (o l'ivar stesso)
  2. confermare sempre weakSelf esiste prima di passarlo come nunnull param

UPDATE:

Mettere un po 'di registrazione intorno allo performBatchUpdates conferma molto:

- (void)logPerformBatchUpdates { 
    [self.collectionView performBatchUpdates:^{ 
     NSLog(@"starting reload"); 
     [self.collectionView reloadItemsAtIndexPaths:[self.collectionView indexPathsForVisibleItems]]; 
     NSLog(@"finishing reload"); 
    } completion:^(BOOL finished) { 
     NSLog(@"completed"); 
    }]; 

    NSLog(@"exiting"); 
} 

stampe:

starting reload 
finishing reload 
exiting 
completed 

Ciò dimostra che il blocco di completamento è generato dopo uscire dall'ambito corrente, il che significa che viene inviato in modo asincrono indietro al thread principale.

Si accenna al fatto che si chiude immediatamente il controller della vista dopo aver eseguito l'aggiornamento batch. Penso che questa sia la radice del tuo problema:

Dopo alcuni test, l'unico modo in cui ero in grado di ricreare la perdita di memoria era spedendo il lavoro prima di chiudere. Si tratta di un lungo tiro, ma non il codice simile a questa per caso ?:

- (void)breakIt { 
    // dispatch causes the view controller to get dismissed before the enclosed block is executed 
    dispatch_async(dispatch_get_main_queue(), ^{ 
     [self.collectionView performBatchUpdates:^{ 
      [self.collectionView reloadItemsAtIndexPaths:[self.collectionView indexPathsForVisibleItems]]; 
     } completion:^(BOOL finished) { 
      NSLog(@"completed: %@", self); 
     }]; 
    }); 
    [self.presentationController.presentingViewController dismissViewControllerAnimated:NO completion:nil]; 
} 

Il codice di cui sopra porta a dealloc non essere chiamato sul controller della vista.

Se si prende il codice esistente e si invia semplicemente (o eseguiSeleziona: dopo :) la chiamata dismissViewController è probabile che risolva anche il problema.

+0

Sei corretto su tutti i fronti, tuttavia, come puoi vedere dal mio snippet di codice sopra, anche con il forte riferimento a se stesso, dovrebbe essere ripulito dopo l'esecuzione del blocco di completamento e il ciclo di conservazione viene rilasciato. Non è chiaro per me come viene creato un ciclo di conservazione e UIViewController non viene mai ridistribuito per fornire il mio frammento di codice specifico sopra. – esilver

+0

il ciclo di mantenimento dovrebbe essere abbastanza chiaro. mantiene automaticamente la vista raccolta e la vista raccolta ora conserva se stessa. uno di questi puntatori deve essere debole per impedire il ciclo di mantenimento (quindi rendere 'weakSelf'). non sono sicuro di quale pulizia ti riferisci dato che UICollectionView è una scatola nera ... NON aspetterei collectionView di annullare il suo blocco di completamento e sicuramente non dovresti fare affidamento su di esso – Casey

+0

Completiamo la tua logica: supponiamo che UICollectionView non nil fuori il suo blocco di completamento. UICollectionView inizia a eseguire aggiornamenti in batch, richiamando il primo blocco (fortemente mantenuto). UIViewController è stato rimosso, ma non può essere pulito perché il secondo blocco di completamento ha un riferimento forte. 100 ms per effetti di animazione. UICollectionView richiama il suo blocco di completamento. Ora tutti i blocchi vengono eseguiti, i riferimenti a detti blocchi devono essere puliti/non ecc. E UIViewController non ha più riferimenti e può dealloc.Questo dovrebbe semplicemente ritardare, non impedire, deallocare. Destra? – esilver

0

Questo sembra un bug con UICollectionView. Gli utenti API non dovrebbero aspettarsi che i parametri dei blocchi a esecuzione singola vengano mantenuti oltre l'esecuzione dell'attività, quindi evitare che i cicli di riferimento non costituiscano un problema.

UICollectionView deve eliminare tutti i riferimenti ai blocchi una volta terminato il processo di aggiornamento batch o se il processo di aggiornamento batch viene interrotto (ad esempio, dalla visualizzazione della raccolta rimossa dallo schermo).

Hai visto da solo che il blocco di completamento viene chiamato anche se la vista dell'insieme viene presa fuori dallo schermo durante il processo di aggiornamento, quindi la vista di raccolta deve quindi annotare qualsiasi riferimento a quel blocco di completamento - non verrà mai più chiamato, indipendentemente dallo stato corrente della vista raccolta.

Problemi correlati