Почему сильная ссылка на родительский UIViewController в executeBatchUpdates теряет активность?
Я только что закончил отладку очень неприятной утечки 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 во время выполнения вызова executeBatchUpdates.
Я искал для обсуждения в Интернете вопрос о том, следует ли использовать шаблон weakSelf/strongSelf в performBatchUpdates
, но не нашел ничего конкретного в этом вопросе.
Я рад, что мне удалось разобраться с этой ошибкой, но мне бы хотелось, чтобы более умный разработчик iOS объяснил мне такое поведение, которое я вижу.
Ответы
Ответ 1
Как вы поняли, когда weak
не используется, создается цикл сохранения.
Цикл сохранения вызван тем, что self
имеет сильную ссылку на collectionView
и collectionView
теперь имеет сильную ссылку на self
.
Следует всегда предполагать, что self
мог быть освобожден до того, как будет выполнен асинхронный блок. Чтобы справиться с этим, необходимо сделать две вещи:
- Всегда используйте слабую ссылку на
self
(или сам ivar)
- Всегда подтверждать
weakSelf
существует, прежде чем передавать его как nunnull
пары
UPDATE:
Ввод немного информации о регистрации 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
не вызывается на контроллере представления.
Если вы берете свой существующий код и просто отправляете (или выполняете вызов Select: после:) dismissViewController
, скорее всего, вы также исправите проблему.
Ответ 2
Это похоже на ошибку с UICollectionView. Пользователи API не должны ожидать сохранения одноблочных параметров блока за пределами выполнения задачи, поэтому предотвращение проблемных циклов не должно быть проблемой.
UICollectionView должен очищать любые ссылки на блоки после завершения процесса пакетного обновления или если процесс пакетного обновления прерывается (например, когда вид коллекции удаляется с экрана).
Вы сами убедились в том, что блок завершения вызывается, даже если просмотр коллекции во время процесса удаления отключен от экрана, поэтому в представлении коллекции должно быть указано, какая ссылка имеет этот блок завершения - это будет никогда не будет вызываться снова, независимо от текущего состояния представления коллекции.