与performBatchUpdates崩溃的恶梦

在收集视图的performBatchUpdates期间,我正面临崩溃的噩梦。

问题基本上是这样的:我在服务器上的目录上有很多图像。 我想在集合视图中显示这些文件的缩略图。 但是缩略图必须从服务器asynchronous下载。 当他们到达他们将被插入收集视图使用这样的事情:

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个文件到服务器。 应用程序看到新文件并开始下载缩略图。 当缩略图下载时,它们将被插入到集合视图中。 但是因为下载可能需要不同的时间,而且这个下载操作是asynchronous的,所以iOS会丢失跟踪这个集合有多less元素,并且整个事情会随着这个灾难性的臭名昭着的消息而崩溃。

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

certificate我有一些可疑的事情是,如果我打印数据集上的项目的数量,我确切地看到213.因此,数据集匹配正确的数字和消息是无稽之谈。

我以前有过这个问题,但是这是一个iOS 7项目。 不知何故现在在iOS 8上返回的问题,那里的解决scheme不工作,现在的数据集IS IN SYNC

我认为这个问题是由索引造成的。

键:

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

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

 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 } } 

这听起来像是你需要做一些额外的工作,分批处理每个animation“组”都出现了哪些图像。 从处理这样的崩溃之前,“performBatchUpdates”的工作方式是

  1. 在调用你的块之前,它会双重检查所有的项目计数,并通过调用'numberOfItemsInSection'(这是错误消息中的154)来保存它们。
  2. 它运行该块,跟踪插入/删除,并根据插入和删除来计算项目的最终数量。
  3. 块运行后,当它询问你的dataSource的'numberOfItemsInSection'(这是213的数字)时,它重复检查它计算的计数到实际的计数。 如果不匹配,将会崩溃。

根据您的variables'insertedIndexes'和'changedIndexes',您将根据服务器的下载响应预先计算需要显示哪些内容,然后运行批处理。 不过,我猜你的'numberOfItemsInSection'方法总是只返回“真”的项目数。

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

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

更难的解决scheme:根据需要执行批次,并跟踪哪些项目已经显示/正在与项目总数分开进行animation制作。 就像是:

 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; }]; }); } 

之后唯一要解决的问题是,如果在animation期间完成最后一个要下载的项目(也许在完成块中运行一个方法“performFinalAnimationIfNeeded”),请确保对剩余项目执行一个最终检查。