performBatchUpdates 的噩梦崩溃

Posted

技术标签:

【中文标题】performBatchUpdates 的噩梦崩溃【英文标题】:Nightmare with performBatchUpdates crash 【发布时间】:2016-06-15 22:10:44 【问题描述】:

我在performBatchUpdatescollection view 上面临着一场噩梦。

问题基本上是这样的:我在服务器上的目录中有很多图像。我想在collection view 上显示这些文件的缩略图。但缩略图必须从服务器异步下载。当它们到达时,它们将使用以下方式插入到集合视图中:

dispatch_async(dispatch_get_main_queue(),
             ^
               [self.collectionView performBatchUpdates:^

                 if (removedIndexes && [removedIndexes count] > 0) 
                   [self.collectionView deleteItemsAtIndexPaths:removedIndexes];
                 

                 if (changedIndexes && [changedIndexes count] > 0) 
                   [self.collectionView reloadItemsAtIndexPaths:changedIndexes];
                 

                 if (insertedIndexes && [insertedIndexes count] > 0) 
                   [self.collectionView insertItemsAtIndexPaths:insertedIndexes];
                 

                completion:nil];
             );

问题是这个(我认为)。假设在时间 = 0 时,集合视图有 10 个项目。然后我再向服务器添加 100 个文件。应用程序看到新文件并开始下载缩略图。随着缩略图的下载,它们将被插入到集合视图中。但是因为下载可能需要不同的时间,而且这个下载操作是asynchronousios 会在某一时刻失去对集合有多少元素的跟踪,并且整个事情都会因为这个灾难性的臭名昭著的消息而崩溃。

*** 由于未捕获的异常“NSInternalInconsistencyException”而终止应用程序,原因:“无效更新:无效 第 0 节中的项目数。包含在第 0 节中的项目数 更新后的现有节(213)必须等于 更新前该部分中包含的项目 (154),加号或减号 从该部分插入或删除的项目数(40 已插入,0 已删除)并加上或减去移入的项目数 或移出该部分(0 移入,0 移出)。'

我有一些可疑之处的证据是,如果我打印数据集上的项目数,我会看到准确的 213。因此,数据集匹配正确的数字并且消息是无意义的。

我以前遇到过这个问题,here 但那是一个 iOS 7 项目。不知何故,现在在 iOS 8 上又出现了问题,并且那里的解决方案不起作用,现在数据集 IS IN SYNC

【问题讨论】:

【参考方案1】:

听起来您需要做一些额外的工作来批处理每个动画group 出现的图像。从以前处理这样的崩溃来看,performBatchUpdates 的工作方式是

    在调用您的块之前,它会再次检查所有项目计数并通过调用 numberOfItemsInSection 保存它们(这是您的错误消息中的 154)。 它运行块,跟踪插入/删除,并根据插入和删除计算应该的最终项目数。 块运行后,当它询问您的数据源numberOfItemsInSection(这是 213 数字)时,它会仔细检查计算的计数与实际计数。如果不匹配,则会崩溃。

根据您的变量insertedIndexeschangedIndexes,您正在根据来自服务器的下载响应预先计算需要显示哪些内容,然后运行批处理。但是我猜你的 numberOfItemsInSection 方法总是只返回“真实”的项目数。

因此,如果在第 2 步中完成下载,当它在“3”中执行完整性检查时,您的号码将不再排列。

最简单的解决方案:等到所有文件都下载完毕,然后执行单个batchUpdates。可能不是最好的用户体验,但它避免了这个问题。

更难的解决方案:根据需要执行批处理,并从项目总数中单独跟踪哪些项目已经显示/当前正在制作动画。类似的东西:

BOOL _performingAnimation;
NSInteger _finalItemCount;

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section 
    return _finalItemCount;


- (void)somethingDidFinishDownloading 
    if (_performingAnimation) 
        return;
    
    // Calculate changes.
    dispatch_async(dispatch_get_main_queue(),
             ^
                _performingAnimation = YES;
               [self.collectionView performBatchUpdates:^

                 if (removedIndexes && [removedIndexes count] > 0) 
                   [self.collectionView deleteItemsAtIndexPaths:removedIndexes];
                 

                 if (changedIndexes && [changedIndexes count] > 0) 
                   [self.collectionView reloadItemsAtIndexPaths:changedIndexes];
                 

                 if (insertedIndexes && [insertedIndexes count] > 0) 
                   [self.collectionView insertItemsAtIndexPaths:insertedIndexes];
                 

                 _finalItemCount += (insertedIndexes.count - removedIndexes.count);
                completion:^
                 _performingAnimation = NO;
               ];
             );

在那之后唯一要解决的问题是,如果最后一个要下载的项目在动画期间完成,请确保对剩余项目进行最后一次检查(可能有一个方法 performFinalAnimationIfNeeded 在完成块中运行)

【讨论】:

【参考方案2】:

我认为问题是由索引引起的。

键:

对于更新和删除的项目,索引必须是原始项目的索引。 对于插入的项目,索引必须是最终项目的索引。

这是一个带有 cmets 的演示代码:

class CollectionViewController: UICollectionViewController 

    var items: [String]!

    let before = ["To Be Deleted 1", "To Be Updated 1", "To Be Updated 2", "To Be Deleted 2", "Stay"]
    let after = ["Updated 1", "Updated 2", "Added 1", "Stay", "Added 2"]

    override func viewDidLoad() 
        super.viewDidLoad()

        self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Refresh", style: .Plain, target: self, action: #selector(CollectionViewController.onRefresh(_:)))

        items = before
    

    func onRefresh(_: AnyObject) 

        items = after

        collectionView?.performBatchUpdates(
            self.collectionView?.deleteItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 3, inSection: 0), ])

            // Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete and reload the same index path
            // self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 1, inSection: 0), ])

            // NOTE: Have to be the indexes of original list
            self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 1, inSection: 0), NSIndexPath(forRow: 2, inSection: 0), ])

            // Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert item 4 into section 0, but there are only 4 items in section 0 after the update'
            // self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0), NSIndexPath(forRow: 5, inSection: 0), ])

            // NOTE: Have to be index of final list
            self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 2, inSection: 0), NSIndexPath(forRow: 4, inSection: 0), ])

        , completion: nil)
    

    override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int 
        return 1
    
    override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
        return items.count
    

    override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell 
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath)

        let label = cell.viewWithTag(100) as! UILabel

        label.text = items[indexPath.row]

        return cell
    

【讨论】:

【参考方案3】:

对于任何有类似问题的人,让我引用UICollectionView 上的文档:

如果在调用此方法之前集合视图的布局不是最新的,则可能会发生重新加载。为避免出现问题,您应该在更新块内更新您的数据模型或确保在调用 performBatchUpdates(_:completion:) 之前更新布局。

我最初引用了一个单独模型对象的数组,但决定在视图控制器中保留该数组的本地副本,并在 performBatchUpdates(_:completion:) 中更新该数组。

问题解决了。

【讨论】:

【参考方案4】:

这可能会发生,因为您还需要确保使用 collectionViews 删除和插入部分。当您尝试在不存在的部分中插入项目时,您将遇到此崩溃。

当您在 X+1、X 处插入项目时,Preform Batch 更新不知道您打算添加第 X+1 节。而您还没有添加该节。

【讨论】:

以上是关于performBatchUpdates 的噩梦崩溃的主要内容,如果未能解决你的问题,请参考以下文章

CollectionView + UIKit Dynamics 在 performBatchUpdates 上崩溃:

尝试隐藏页脚/页眉时,collectionView.performBatchUpdates 崩溃

为 collectionview 链接 performBatchUpdates

UITableView 意外反弹 beginUpdates()/endUpdates()/performBatchUpdates()

UICollectionView performBatchUpdates:动画所有部分

多个 CollectionView PerformBatchUpdates