performBatchUpdates 崩溃的噩梦
Nightmare with performBatchUpdates crash
我在 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 会忘记集合有多少元素并且整个事情会因为这个灾难性的臭名昭著的消息而崩溃。
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of items in section 0. The number of items contained in an
existing section after the update (213) must be equal to the number of
items contained in that section before the update (154), plus or minus
the number of items inserted or deleted from that section (40
inserted, 0 deleted) and plus or minus the number of items moved into
or out of that section (0 moved in, 0 moved out).'
我发现有些可疑的证据是,如果我打印数据集上的项目计数,我会准确地看到 213。因此,数据集与正确的数字匹配,消息是无意义的。
我以前遇到过这个问题, 但那是一个 iOS 7 项目。不知何故,问题现在在 iOS 8 上再次出现,那里的解决方案不起作用,现在数据集 IS IN SYNC.
听起来您需要做一些额外的工作来对每个动画出现的图像进行批处理 group
。从处理像这样的崩溃开始,performBatchUpdates
的工作方式是
- 在调用您的块之前,它会仔细检查所有项目计数并通过调用
numberOfItemsInSection
保存它们(这是您的错误消息中的 154)。
- 它 运行 是块,跟踪 inserts/deletes,并根据插入和删除计算 应该 的最终项目数。
- 在块 运行 之后,它会在询问您的数据源
numberOfItemsInSection
(这是 213 数字)时,将计算的计数与实际计数进行双重检查。如果不匹配,它将崩溃。
根据您的变量 insertedIndexes
和 changedIndexes
,您根据服务器的下载响应预先计算需要显示哪些内容,然后 运行批。但是我猜你的 numberOfItemsInSection
方法总是只返回 'true' 个项目数。
因此,如果下载在步骤 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
你 运行 在完成块中)
我认为问题是由索引引起的。
键:
- 对于更新和删除的项目,索引必须是原始项目的索引。
- 对于插入项,索引必须是最终项的索引。
这是一个带注释的演示代码:
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
}
}
对于遇到类似问题的任何人,让我引用 UICollectionView
上的文档:
If the collection view's layout is not up to date before you call this method, a reload may occur. To avoid problems, you should update your data model inside the updates block or ensure the layout is updated before you call performBatchUpdates(_:completion:)
.
我最初引用了一个单独模型对象的数组,但决定在视图控制器中保留数组的本地副本并在 performBatchUpdates(_:completion:)
中更新数组。
问题已解决。
这可能会发生,因为您确实还需要确保使用 collectionViews 来删除和插入部分。当您尝试在不存在的部分中插入项目时,您将遇到此崩溃。
当您在 X+1 处插入项目时,Preform Batch updates 不知道您打算添加第 X+1 部分,而您还没有添加该部分。
我在 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 会忘记集合有多少元素并且整个事情会因为这个灾难性的臭名昭著的消息而崩溃。
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (213) must be equal to the number of items contained in that section before the update (154), plus or minus the number of items inserted or deleted from that section (40 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'
我发现有些可疑的证据是,如果我打印数据集上的项目计数,我会准确地看到 213。因此,数据集与正确的数字匹配,消息是无意义的。
我以前遇到过这个问题,
听起来您需要做一些额外的工作来对每个动画出现的图像进行批处理 group
。从处理像这样的崩溃开始,performBatchUpdates
的工作方式是
- 在调用您的块之前,它会仔细检查所有项目计数并通过调用
numberOfItemsInSection
保存它们(这是您的错误消息中的 154)。 - 它 运行 是块,跟踪 inserts/deletes,并根据插入和删除计算 应该 的最终项目数。
- 在块 运行 之后,它会在询问您的数据源
numberOfItemsInSection
(这是 213 数字)时,将计算的计数与实际计数进行双重检查。如果不匹配,它将崩溃。
根据您的变量 insertedIndexes
和 changedIndexes
,您根据服务器的下载响应预先计算需要显示哪些内容,然后 运行批。但是我猜你的 numberOfItemsInSection
方法总是只返回 'true' 个项目数。
因此,如果下载在步骤 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
你 运行 在完成块中)
我认为问题是由索引引起的。
键:
- 对于更新和删除的项目,索引必须是原始项目的索引。
- 对于插入项,索引必须是最终项的索引。
这是一个带注释的演示代码:
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
}
}
对于遇到类似问题的任何人,让我引用 UICollectionView
上的文档:
If the collection view's layout is not up to date before you call this method, a reload may occur. To avoid problems, you should update your data model inside the updates block or ensure the layout is updated before you call
performBatchUpdates(_:completion:)
.
我最初引用了一个单独模型对象的数组,但决定在视图控制器中保留数组的本地副本并在 performBatchUpdates(_:completion:)
中更新数组。
问题已解决。
这可能会发生,因为您确实还需要确保使用 collectionViews 来删除和插入部分。当您尝试在不存在的部分中插入项目时,您将遇到此崩溃。
当您在 X+1 处插入项目时,Preform Batch updates 不知道您打算添加第 X+1 部分,而您还没有添加该部分。