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

Я счастлив, что мне удалось добраться до сути этой ошибки, но я хотел бы, чтобы более умный разработчик iOS объяснил мне это поведение, которое я вижу.
2 10

2 ответа:

Как вы выяснили, когда weak не используется, создается цикл удержания.

Цикл удержания вызван тем, что self имеет сильную ссылку на collectionView и collectionView теперь имеет сильную ссылку на self.

Всегда следует предполагать, что self могло быть освобождено до выполнения асинхронного блока. Чтобы справиться с этим безопасно, необходимо сделать две вещи:

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

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