Почему сильная ссылка на родительский UIViewController в performBatchUpdates приводит к утечке действия?
Я только что закончил отладку очень неприятной утечки UIViewController, такой, что UIViewController не был освобожден даже после вызова dismissViewControllerAnimated.
Я отследил проблему до следующего блока кода:
self.dataSource.doNotAllowUpdates = YES;
[self.collectionView performBatchUpdates:^{
[self.collectionView reloadItemsAtIndexPaths:@[indexPath]];
} completion:^(BOOL finished) {
self.dataSource.doNotAllowUpdates = NO;
}];
В принципе, если я позвоню в performBatchUpdates а потом сразу звоните dismissViewControllerAnimated, UIViewController получает утечку и метод dealloc этого UIViewController никогда не вызывается. UIViewController висит вокруг вечно.
performBatchUpdates выполняется в течение некоторого интервала времени, скажем, 500 мс, поэтому я бы предположил, что после указанного интервала он вызовет эти методы, а затем вызовет dealloc.
Исправление выглядит следующим образом:
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;
}
}];
Обратите внимание, что переменная-член BOOL, doNotAllowUpdates, является переменной, которую я добавил, что предотвращает любые обновления dataSource / collectionView во время выполнения вызова performBatchUpdates.
Я искал в интернете дискуссию о том, следует ли нам использовать паттерн weakSelf/strongSelf в performBatchUpdates, но ничего конкретно по этому вопросу не нашел.
2 ответа:
Как вы выяснили, когда
weakне используется, создается цикл удержания.Цикл удержания вызван тем, что
selfимеет сильную ссылку наcollectionViewиcollectionViewтеперь имеет сильную ссылку наself.Всегда следует предполагать, что
selfмогло быть освобождено до выполнения асинхронного блока. Чтобы справиться с этим безопасно, необходимо сделать две вещи:
- всегда используйте слабую ссылку на
self(или сам Ивар)- всегда подтверждайте, что
weakSelfсуществует до передавая его какnunnullparamОбновление:
Немного протоколирования
performBatchUpdatesподтверждает многое:- (void)logPerformBatchUpdates { [self.collectionView performBatchUpdates:^{ NSLog(@"starting reload"); [self.collectionView reloadItemsAtIndexPaths:[self.collectionView indexPathsForVisibleItems]]; NSLog(@"finishing reload"); } completion:^(BOOL finished) { NSLog(@"completed"); }]; NSLog(@"exiting"); }Отпечатки пальцев:
starting reload finishing reload exiting completedЭто показывает, что блок завершения запускается после выхода из текущей области видимости, что означает, что он асинхронно отправляется обратно в основной поток.
Вы упоминаете, что немедленно отключаете контроллер Вида после выполнения пакетного обновления. Я думаю, что это корень вашего выпуск:
После некоторого тестирования единственным способом, которым я смог воссоздать утечку памяти, была отправка работы перед увольнением. Это рискованно, но ваш код случайно не выглядит так?:
Приведенный выше код приводит к тому, что- (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]; }deallocне вызывается на контроллере вида.Если вы возьмете существующий код и просто отправите (или performSelector:after:) вызов
dismissViewController, вы, вероятно, также исправите проблему.
Это похоже на ошибку с UICollectionView. Пользователи API не должны ожидать, что параметры одиночного блока будут сохранены после выполнения задачи, поэтому предотвращение ссылочных циклов не должно быть проблемой.
UICollectionView должен очищать любые ссылки на блоки после завершения процесса пакетного обновления или если процесс пакетного обновления прерван (например, при удалении представления коллекции с экрана).
Вы сами видели, что блок завершения вызывается даже в том случае, если представление коллекции удалено с экрана во время процесса обновления, поэтому представление коллекции должно быть затем вычеркнуто из любой ссылки, которая у него есть на этот блок завершения-он никогда не будет вызван снова, независимо от текущего состояния представления коллекции.