动态更改数据源导致 deleteRowsAtIndexPaths:indexes 崩溃
Posted
技术标签:
【中文标题】动态更改数据源导致 deleteRowsAtIndexPaths:indexes 崩溃【英文标题】:Dynamically changing data source causing deleteRowsAtIndexPaths:indexes to crash 【发布时间】:2015-06-30 17:34:00 【问题描述】:为了让它发挥作用,我把头发扯掉了。我要执行[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
,
我如何删除的更详细代码:
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
这可以正常工作,但是如果我在删除应用程序时收到推送通知(即收到新消息)将崩溃并显示如下错误:
-[UITableView _endCellAnimationsWithContext:] 中的断言失败, /SourceCache/UIKit/UIKit-3347.44/UITableView.m:1327 2015-07-04 19:12:48.623 myapp[319:24083] *** 由于未捕获的异常“NSInternalInconsistencyException”而终止应用程序,原因: '尝试从仅包含 1 行的第 0 节中删除第 1 行 更新前'
我怀疑这是因为我的数据源在变化,数组的大小在变化
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
删除时的引用将不一致,因为当推送通知触发刷新时它会增加一。有什么办法可以解决这个问题吗? deleteRowsAtIndexPaths
使用 numberOfRowsInSection
方法我是否正确?
【问题讨论】:
您是否尝试在deleteRowsAtIndexPaths
方法调用周围添加[self.tableView beginUpdates];
和[self.tableView endUpdates];
?
我试过了(虽然没有 dispatch_async)
用 dispatch_async 试试?
也没有运气
为什么不在deleteRowsAtIndexPaths
调用之后在第一个块内调度第二个块?如果它是异步的,这应该可以工作,至少如果 deleteRowsAtIndexPaths
是一个阻塞调用。
【参考方案1】:
因此,为了解决您的问题,您需要确保在某些表格视图动画到位时您的数据源不会改变。我建议做以下事情。
首先,创建两个数组:messagesToDelete
和 messagesToInsert
。这些将包含有关您要删除/插入哪些消息的信息。
其次,将布尔属性 updatingTable
添加到表视图数据源。
三、添加如下功能:
-(void)updateTableIfPossible
if (!updatingTable)
updatingTable = [self updateTableViewWithNewUpdates];
-(BOOL)updateTableViewWithNewUpdates
if ((messagesToDelete.count == 0)&&(messagesToInsert.count==0))
return false;
NSMutableArray *indexPathsForMessagesThatNeedDelete = [[NSMutableArray alloc] init];
NSMutableArray *indexPathsForMessagesThatNeedInsert = [[NSMutableArray alloc] init];
// for deletion you need to use original messages to ensure
// that you get correct index paths if there are multiple rows to delete
NSMutableArray *oldMessages = [self.messages copy];
for (id message in messagesToDelete)
int index = (int)[self.oldMessages indexOfObject:message];
[self.messages removeObject:message];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
[indexPathsForMessagesThatNeedDelete addObject:indexPath];
for (id message in messagesToInsert)
[self.messages insertObject:message atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[indexPathsForMessagesThatNeedInsert addObject:indexPath];
[messagesToDelete removeAllObjects];
[messagesToInsert removeAllObjects];
// at this point your messages array contains
// all messages which should be displayed at CURRENT time
// now do the following
[CATransaction begin];
[CATransaction setCompletionBlock:^
updatingTable = NO;
[self updateTableIfPossible];
];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:indexPathsForMessagesThatNeedDelete withRowAnimation:UITableViewRowAnimationLeft];
[tableView insertRowsAtIndexPaths:indexPathsForMessagesThatNeedInsert withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
[CATransaction commit];
return true;
最后,您需要在所有想要添加/删除行的函数中包含以下代码。
添加消息
[self.messagesToInsert addObject:message];
[self updateTableIfPossible];
删除消息
[self.messagesToDelete addObject:message];
[self updateTableIfPossible];
此代码的作用是确保数据源的稳定性。只要有更改,您就可以将需要插入/删除的消息添加到数组中(messagesToDelete
和messagesToDelete
)。然后调用函数updateTableIfPossible
,如果当前没有动画正在进行,它将更新表格视图的数据源(并将动画更改)。如果有一个动画正在进行中,它在这个阶段什么都不做。
但是,因为我们添加了补全
[CATransaction setCompletionBlock:^
updatingTable = NO;
[self updateTableIfPossible];
];
在动画结束时,我们的数据源将检查是否有任何新的更改需要应用于表格视图,如果有,它将更新动画。
这是一种更安全的更新数据源的方法。请让我知道它是否适合您。
【讨论】:
如果有人在桌子上打电话reloadData
会怎样?
如果你想支持有/没有动画的混合行为,你需要添加额外的逻辑。例如,创建一个函数 instantUpdate,它将修改您的数据源并在表上调用 reloadData。只要确保您处理另一个动画运行的可能性
我实际上正在慢慢尝试您建议的方法,我确实有一些 reloadData 但我可能能够排除这些。原因是,不知何故,tableview 仍然“存在”,但实际上是隐藏的 - 在那些时候根本不需要插入/删除/重新加载数据(我确实希望插入/删除的负载非常高)。只在messagesToDelete
和messagesToPost
中建立“要做的工作”,然后在tableview 再次可见之前将它们全部完成。有了那个 reloadData 就不需要了……这是我现在正在考虑的计划。
我长期使用(Swift 风格)这种方法,真的可以推荐它。我仍然看到一些崩溃,几乎完全是在流量高峰时 - 当然我自己无法重现它。我现在怀疑 CATransaction setCompletionBlock - self 内部可能很弱 - 以防视图控制器(拥有此代码块的任何东西)被释放。
@Jonny 是的,释放可能是个问题。我最近在我的一个应用程序中遇到了表格视图崩溃,如果用户开始超快速滚动然后在表格滚动时退出,就会发生这种情况。听起来不太可能——但你永远不知道你的用户会做什么:D p.s.崩溃是另一个原因,但它仍然说明了这一点:)【参考方案2】:
删除一行
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
删除部分
注意:如果您的 TableView 有多个部分,则当部分仅包含一行而不是删除行时,您必须删除整个部分
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexSet indexSetWithIndex:0];
[tableView beginUpdates];
[tableView deleteSections:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
【讨论】:
【参考方案3】:我对上面的代码进行了一些测试。你不能同时做到这一点,我们至少可以做的是:
//This will wait for `deleteRowsAtIndexPaths:indexes` before the `setCompletionBlock `
//
[CATransaction begin];
[CATransaction setCompletionBlock:^
[tableView reloadData];
];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
[CATransaction commit];
在你的代码中:
/*
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
*/
这是不安全的,因为如果通知被触发并且由于某种原因在deleteRowsAtIndexPaths
之前调用了[self.tableView reloadData];
,这将导致崩溃,因为tableView 当前正在更新数据然后被deleteRowsAtIndexPaths:
中断,请尝试这个序列要检查:
/*
...
[self.tableView reloadData];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
This will cause a crash...
*/
嗯.. 回到您的代码,让我们进行一个可能导致崩溃的模拟.. 这只是一个假设,所以这并不是 100% 确定的 (99%)。 :)
假设self.messageToDelete
等于零;
int index = (int)[self.messages indexOfObject: nil];
// since you casted this to (int) with would be 0, so `index = 0`
[self.messages removeObject: nil];
// self.messages will remove nil object which is non existing therefore
// self.messages is not updated/altered/changed
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
// indexPath.row == 0
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
// then you attempted to delete index indexPath at row 0 in section 0
// but the datasource is not updated meaning this will crash..
//
// Same thing will happen if `self.messageToDelete` is not existing in your `self.messages `
我的建议是先检查self.messageToDelete
:
if ([self.messages containsObject:self.messageToDelete])
// your delete code...
希望这有帮助,干杯! ;)
【讨论】:
我们为什么要调用[tableView reloadData]? @Kex,等我编辑可能会回答并添加更多解释,我认为是什么导致了崩溃。 我试过取出所有 [self.tableView reloadData] ,但它仍然崩溃。我认为当调用删除操作时,它首先使用委托方法检查行数。问题就在方法完成之前,数组大小增加了 1,因此它不一致并崩溃......(无论如何我的理论) @Kex,你确定self.messageToDelete
在删除后刷新了吗?也许您正在删除不存在的对象。我建议您在调用deleteRowsAtIndexPaths
之前先检查self.messageToDelete
是否存在于数组中,并确保self.messageToDelete
不为零。【参考方案4】:
'尝试从第 0 部分删除第 1 行,更新前只包含 1 行'
这表示您的索引路径,在删除过程中,引用第 1 行。但是第 1 行不存在。只有第 0 行存在(与该部分一样,它们是从零开始的)。
那么,如何让 indexPath 比元素数大一?
我们可以看到插入行的通知处理程序吗?如果动画正在进行中,您可以做一些技巧,在通知处理期间执行Selector:withDelay: 以留出时间让动画完成。
【讨论】:
一直在做一些检查。你是对的,问题是当新消息进来时数组大小发生了变化。因此,如果一条消息之前的索引为 0,而一条新消息到达,则数组会更新,即将删除的消息现在位于索引 1。如果表在 deleteRowsAtIndexPaths 之前没有及时刷新,它将崩溃。不知道如何去修复它。我需要延迟编程吗? 听起来您可能正在后台线程中处理新消息。如果是这样,让它 performSelector:onMainThread: 或通过 dispatch_async 到主队列。这样,当您的其他代码正在删除该行时,它就无法更改数据结构。【参考方案5】:在您的代码中,崩溃的原因是:您正在更新数组但没有更新UITableView
的数据源。因此,您的 dataSource 还需要在调用 endUpdates 时反映更改。
因此根据 Apple 文档。
ManageInsertDeleteRow
要对行和部分的批量插入、删除和重新加载进行动画处理,请在由连续调用 beginUpdates 和 endUpdates 定义的动画块中调用相应的方法。如果你不调用这个块内的插入、删除和重新加载方法,行和节的索引可能是无效的。对 beginUpdates 和 endUpdates 的调用可以嵌套;所有索引都被视为只有外部更新块。
在一个块结束时——即在 endUpdates 返回之后——表视图查询它的数据源并像往常一样为行和节数据进行委托。因此,支持 table view 的集合对象应该更新以反映新的或删除的行或节。
示例:
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
希望对您有所帮助。
【讨论】:
【参考方案6】:我认为这里的许多 cmets 都提到了答案,但我会试着梳理一下。
第一个问题是您似乎没有在 beginUpdates
/endUpdates
方法中将删除方法调用括起来。这是我要解决的第一件事,否则正如 Apple 文档中所警告的那样,事情可能会出错。 (这并不意味着他们会出错,这样做只会更安全。)
完成此操作后,重要的是调用endUpdates
会检查数据源,以确保调用numberOfRows
会考虑任何插入或删除。例如,如果您删除一行(如您所做的那样),当您调用endUpdates
时,您最好确保您还从数据源中删除了一个项目。看起来您正在做这部分是正确的,因为您要从 self.messages
中删除一个项目,我认为这是您的数据源。
(顺便说一句,你是正确的,在 beginUpdates
/endUpdates
中单独调用 deleteRows...
而不在 beginUpdates
/endUpdates
中也调用 numberOfRows
在数据源上,您可以通过在其中放置一个断点来轻松检查。但是再说一次,不要这样做,总是使用beginUpdates
/endUpdates
。)
现在 可能在调用 deleteRows...
和 endUpdates
之间,其他人正在修改 self.messages
数组,通过添加一个对象,但我不知道我是否真的相信那是因为它必须非常不幸地定时。更有可能的是,当您收到推送消息时,您没有正确处理行的插入,再次是不使用beginUpdates
/endUpdates
,或者做错了其他事情。如果您可以发布处理插入的代码部分,将会很有帮助。
如果该部分看起来没问题,那么在调用删除代码时,您确实很不幸地在另一个线程上对 self.messages
数组进行了更改。您可以通过添加一个日志行来检查这一点,您的插入代码会在其中向数组添加一条消息:
NSLog(@"Running on %@ thread", [NSThread currentThread]);
或者只是在其中放置一个断点并查看您最终在哪个线程上。如果这确实是问题所在,您可以通过执行以下操作来分派修改数组并将行插入主线程的代码(无论如何它应该在主线程中,因为它是一个 UI 操作):
dispatch_async(dispatch_get_main_queue(), ^
[self.messages addObject:newMessage];
[self.tableView beginUpdates];
[self.tableView insertRows...]
[self.tableView endUpdates];
);
这将确保消息数组在主线程忙于删除行时不会被另一个线程改变。
【讨论】:
以上是关于动态更改数据源导致 deleteRowsAtIndexPaths:indexes 崩溃的主要内容,如果未能解决你的问题,请参考以下文章
带有 ngmodel 的 Angular 2 动态表单示例导致“表达式在检查后已更改”
当动态更改背景时,Xamarin Custom Frame会导致Java.Lang.NullPointerException