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

Вы сами убедились в том, что блок завершения вызывается, даже если просмотр коллекции во время процесса удаления отключен от экрана, поэтому в представлении коллекции должно быть указано, какая ссылка имеет этот блок завершения - это будет никогда не будет вызываться снова, независимо от текущего состояния представления коллекции.