动态更改数据源导致 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】:

因此,为了解决您的问题,您需要确保在某些表格视图动画到位时您的数据源不会改变。我建议做以下事情。

首先,创建两个数组:messagesToDeletemessagesToInsert。这些将包含有关您要删除/插入哪些消息的信息。

其次,将布尔属性 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];

此代码的作用是确保数据源的稳定性。只要有更改,您就可以将需要插入/删除的消息添加到数组中(messagesToDeletemessagesToDelete)。然后调用函数updateTableIfPossible,如果当前没有动画正在进行,它将更新表格视图的数据源(并将动画更改)。如果有一个动画正在进行中,它在这个阶段什么都不做。

但是,因为我们添加了补全

[CATransaction setCompletionBlock:^
     updatingTable = NO;
     [self updateTableIfPossible];
];

在动画结束时,我们的数据源将检查是否有任何新的更改需要应用于表格视图,如果有,它将更新动画。

这是一种更安全的更新数据源的方法。请让我知道它是否适合您。

【讨论】:

如果有人在桌子上打电话reloadData会怎样? 如果你想支持有/没有动画的混合行为,你需要添加额外的逻辑。例如,创建一个函数 instantUpdate,它将修改您的数据源并在表上调用 reloadData。只要确保您处理另一个动画运行的可能性 我实际上正在慢慢尝试您建议的方法,我确实有一些 reloadData 但我可能能够排除这些。原因是,不知何故,tableview 仍然“存在”,但实际上是隐藏的 - 在那些时候根本不需要插入/删除/重新加载数据(我确实希望插入/删除的负载非常高)。只在messagesToDeletemessagesToPost 中建立“要做的工作”,然后在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

在 AfterViewInit 中更新布尔值会导致“检查后表达式已更改”

动态更改详细视图控制器

网站动态 | Bash Shell 处理 JSON 数据

具有动态高度的水平 CollectionView 导致 FlowLayout 警告