为什么在performBatchUpdates中对父UIViewController的强引用会导致内存泄漏?

10

我刚刚修复了一个非常棘手的UIViewController泄漏问题,即使调用dismissViewControllerAnimated后也不能释放UIViewController

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

    self.dataSource.doNotAllowUpdates = YES;

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

基本上,如果我调用performBatchUpdates,然后立即调用dismissViewControllerAnimated,UIViewController 就会泄漏,这个 UIViewController 的 dealloc 方法永远不会被调用,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是我添加的变量,它可以防止在执行performBatchUpdates时进行任何种类的数据源/集合视图更新。
我搜索了一下关于是否应在performBatchUpdates中使用weakSelf/strongSelf模式的在线讨论,但没有找到具体针对这个问题的内容。
我很高兴能够找到此错误的根本原因,但我希望有更聪明的iOS开发人员能解释一下我看到的这种行为。

很有意思看到你是否能够确定是更新块还是完成块导致了保留循环。正如你所说,两者都不应该永久地保留它们的块 - 我只能假设当集合视图从其父视图中删除时,它停止运行任何批处理更新并且不会调用或释放完成块。在我看来这值得一份 radar(苹果应用程序缺陷报告) 。 - jrturton
@jrturton 哦,这似乎是最有可能的解释!根据这个洞察力,我会看看能否在调试器中重现这个问题。 - esilver
@jrturton 不幸的是,我无法在调试器中重现...至少似乎两个块总是被调用。也许如果dismiss在中间被调用,内部并没有将这些块nil化? - esilver
我会提交一个雷达报告。正如您在答案评论中所说,这里不应该创建保留周期 - 像这样的单次块应该在执行后被清除。 - jrturton
@jrturton 好的,我会做的。如果你回答这个问题,我会接受它,并获得额外的50分奖励,因为我认为那是我所见过的最好的解释。 - esilver
我不认为我应该得到赏金,但我也不会拒绝 ;) - jrturton
2个回答

0

这似乎是UICollectionView的一个bug。API用户不应该期望单次运行的块参数在任务执行结束后被保留,因此防止引用循环不应该成为问题。

UICollectionView应该在批量更新过程完成后清除对块的任何引用,或者如果批量更新过程被中断(例如,通过将集合视图从屏幕上移除),则应该清除对块的任何引用。

您已经亲眼看到即使在更新过程中将集合视图从屏幕上移除,完成块也会被调用,因此集合视图应该将其对该完成块的任何引用置空 - 无论集合视图的当前状态如何,它都不会再次被调用。


-1

正如你所发现的,当不使用weak时会产生保留周期。

保留周期是由于selfcollectionView有强引用,而collectionView现在对self也有了强引用。

在执行异步块之前,必须始终假定self可能已被释放。为了安全处理这一点,必须完成两件事:

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

更新:

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方法没有被调用。
如果你采取现有的代码,并简单地分派(或执行选择器:after:)dismissViewController调用,你很可能也会解决这个问题。

你在所有方面都是正确的,然而,正如你可以从我上面的代码片段中看到的那样,即使有对self的强引用,它也应该在完成块运行并释放保留周期后进行清理。对于我的特定代码片段,我不清楚如何创建保留周期并且UIViewController从未被解除分配。 - esilver
保留循环应该是相当明显的。self 保留了 collection view,而 collection view 现在保留了 self。这些指针中有一个需要是 weak 的,以防止保留循环(从而使 weakSelf)。我不确定您所指的清理工作,因为 UICollectionView 是黑盒子...我不希望 collectionView 将其完成块设置为 nil,并且您绝对不应该依赖它。 - Casey
让我们完成你的逻辑:假设UICollectionView不会将其完成块设置为nil。UICollectionView开始执行批量更新,调用第一个(强引用)块。UIViewController被解除,但无法清理,因为第二个完成块具有强引用。100毫秒用于动画效果。UICollectionView调用其完成块。现在,所有块都已运行,对这些块的引用应该被清理/设置为nil等,而UIViewController没有更多的引用并且可以dealloc。这应该只是延迟,而不是防止dealloc。对吗? - esilver
此外,我在互联网上找不到任何使用这种弱引用模式的performBatchUpdates示例。如果由performBatchUpdates创建的保留循环即使在运行后也永远不会被打破,为什么不是所有应用程序都会泄漏UIViewControllers并崩溃呢? - esilver

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接