为什么在performBatchUpdates中对父UIViewController的强引用会泄漏活动?

esi*_*ver 10 memory-leaks objective-c uiviewcontroller ios uicollectionview

我刚刚完成调试一个非常讨厌的UIViewController泄漏,这样即使在调用之后UIViewController也没有被释放dismissViewControllerAnimated.

我将问题跟踪到以下代码块:

    self.dataSource.doNotAllowUpdates = YES;

    [self.collectionView performBatchUpdates:^{
        [self.collectionView reloadItemsAtIndexPaths:@[indexPath]];
    } completion:^(BOOL finished) {
        self.dataSource.doNotAllowUpdates = NO;
    }];
Run Code Online (Sandbox Code Playgroud)

基本上,如果我调用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;
        }
    }];
Run Code Online (Sandbox Code Playgroud)

请注意,BOOL成员变量,, doNotAllowUpdates是我添加的变量,它在执行performBatchUpdates调用时阻止任何类型的dataSource/collectionView更新.

我在网上搜索关于我们是否应该使用weakSelf/strongSelf模式的讨论performBatchUpdates,但没有在这个问题上找到任何具体内容.

我很高兴我能够找到这个bug的底部,但我希望一个更聪明的iOS开发人员能够向我解释我所看到的这种行为.

Cas*_*sey -1

正如你所想的,当weak不使用时,会创建保留周期。

保留周期是由于self强引用引起的collectionViewcollectionView现在强引用self

人们必须始终假设self在执行异步块之前可能已被释放。为了安全地处理这个问题,必须做两件事:

  1. 始终使用弱引用self始终使用对(或 ivar 本身)的
  2. weakSelf在将其作为nunnull 参数传递之前始终确认其存在

更新:

进行一些日志记录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");
}
Run Code Online (Sandbox Code Playgroud)

印刷:

starting reload
finishing reload
exiting
completed
Run Code Online (Sandbox Code Playgroud)

这表明完成块在离开当前作用域后被触发,这意味着它被异步分派回主线程。

您提到在执行批量更新后立即关闭视图控制器。我认为这是你问题的根源:

经过一些测试后,我能够重新创建内存泄漏的唯一方法是在解雇之前分派工作。虽然可能性不大,但是您的代码是否偶然看起来像这样?:

- (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];
}
Run Code Online (Sandbox Code Playgroud)

上面的代码导致dealloc视图控制器上没有被调用。

如果您采用现有代码并简单地调度(或执行选择器:之后:)dismissViewController调用,您也可能会解决问题。