10

我刚刚完成调试一个非常讨厌的UIViewController泄漏,这样即使在调用dismissViewControllerAnimated之后UIViewController也不会被释放。为什么在performBatchUpdates中对父UIViewController的强引用泄漏了一个活动?

我的问题追查到下面的代码块:

self.dataSource.doNotAllowUpdates = YES; 

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

基本上,如果我做一个呼叫performBatchUpdates后立即调用dismissViewControllerAnimated的UIViewController中被泄露和UIViewControllerdealloc方法不会被调用。 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,是我添加防止任何类型的数据源/的CollectionView更新到performBatchUpdates一个呼叫正在运行时的变量。

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

我很高兴能够深入到这个bug的底部,但我更喜欢更聪明的iOS开发人员向我解释我看到的这种行为。

+0

看看您是否可以确定是否导致保留周期的更新块或完成块,这会很有趣。正如你所说的,它们都不应该永久保留它们的块 - 我只能假设当集合视图从其超级视图中被移除时,它将停止运行任何批量更新,并且不会调用或释放完成块。在我看来,值得雷达。 – jrturton

+0

@jrturton啊这似乎是最有可能的解释!基于这种见解,我会看看是否可以在调试器中重新制作这个代码。 – esilver

+0

@ jrturton alas无法在调试器中重现......看起来好像两个块至少总是被调用。也许内部不是无效的,虽然如果解雇被称为中间? – esilver

回答

-1

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

保留周期是由self引起的,对collectionView有很强的参考,现在collectionViewself有很强的参考。

必须始终假定在执行异步块之前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没有被调用视图控制器上。

如果你把你现有的代码,并简单地派遣(或执行选择器:之后:) dismissViewController调用你可能会解决这个问题。

+0

但是,正如你从上面的代码片断中看到的那样,即使对自我的强烈引用,它也应该在完成块运行并且保持周期被释放之后清理。我不清楚如何创建一个保留周期,并且UIViewController从未被拒绝,请给我上面的特定代码片段。 – esilver

+0

保留周期应该很清楚。自我保留了收藏视图,收藏视图现在保留了自我。其中一个指针需要很弱以防止保留周期(因此产生'weakSelf')。我不确定你指的是什么清理,因为UICollectionView是一个黑匣子...我不希望collectionView将其完成块清零,你绝对不应该依赖它 – Casey

+0

让我们来完成你的逻辑:假设UICollectionView没有没有完成块。 UICollectionView开始执行批量更新,调用第一个(强保留)块。 UIViewController被解雇,但无法清理,因为第二个完成块有很强的参考。 100毫秒过去的动画效果。 UICollectionView调用它的完成块。现在,所有块都运行,对所述块的引用应该被清除/清零等,并且UIViewController没有更多的引用并且可以释放。这应该简单地延迟,而不是阻止,解除分配。对? – esilver

0

这看起来像一个UICollectionView的错误。 API用户不应期望在执行任务之后保留单运行块参数,因此防止参考周期不应成为问题。

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

你已经看到,即使集合视图在更新过程中脱离屏幕,也会调用完成块,因此集合视图应该消除它对该完成块的任何引用 - 它将永远不会再被调用,无论收集视图的当前状态如何。