performBatchUpdates 的噩梦崩溃
Posted
技术标签:
【中文标题】performBatchUpdates 的噩梦崩溃【英文标题】:Nightmare with performBatchUpdates crash 【发布时间】:2016-06-15 22:10:44 【问题描述】:我在performBatchUpdates
collection 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 个文件。应用程序看到新文件并开始下载缩略图。随着缩略图的下载,它们将被插入到集合视图中。但是因为下载可能需要不同的时间,而且这个下载操作是asynchronous
,ios 会在某一时刻失去对集合有多少元素的跟踪,并且整个事情都会因为这个灾难性的臭名昭著的消息而崩溃。
*** 由于未捕获的异常“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 数字)时,它会仔细检查计算的计数与实际计数。如果不匹配,则会崩溃。
根据您的变量insertedIndexes
和changedIndexes
,您正在根据来自服务器的下载响应预先计算需要显示哪些内容,然后运行批处理。但是我猜你的 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()