Почему сильная ссылка на родительский 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
существует до передавая его какnunnull
paramОбновление:
Немного протоколирования
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 должен очищать любые ссылки на блоки после завершения процесса пакетного обновления или если процесс пакетного обновления прерван (например, при удалении представления коллекции с экрана).
Вы сами видели, что блок завершения вызывается даже в том случае, если представление коллекции удалено с экрана во время процесса обновления, поэтому представление коллекции должно быть затем вычеркнуто из любой ссылки, которая у него есть на этот блок завершения-он никогда не будет вызван снова, независимо от текущего состояния представления коллекции.