tl;dr - 请参阅底部的结论。
我也偶尔从用户那里收到有关此的崩溃报告,然后今天为自己遭受了侮辱。这是在我的表格视图上选择一行尝试推动新的导航控制器,然后我按下后退按钮时引起的。奇怪的是,我推送的导航控制器不包含任何数据。按下后,导航栏下的视图变黑,应用崩溃了。
环顾四周,我只能找到其他用户(here、here 和 here),这表明这是由于调用 segue 或快速连续两次推动视图控制器。但是,我只在我的tableView:didSelectRowAtIndexPath: 中调用pushViewController: 一次。没有用户双击一个单元格,我不明白为什么这会发生在我身上。通过双击表格视图单元格进行的测试未能重现观察到的崩溃。
但是,如果用户双击了,因为当时主线程恰好被阻塞了怎么办? (我知道,我知道,永远不要阻塞主线程!)
为了测试这一点,我创建了一些(相当可怕,抱歉,从其他代码快速剪切和粘贴)类方法(在虚构类 MyDebugUtilities 中)来反复阻塞和解除阻塞主线程,在我打开之前启动它包含导致崩溃的表视图的视图控制器。这是任何想要自己快速检查的人的代码(对[MyDebugUtilities repeatedlyBlockTheMainThread]; 的调用将使用信号量阻塞主线程 3 秒,然后在 3 秒后排队另一个对重复BlockTheMainThread 的调用,无限期。顺便说一句,你不要'不想重复启动这个方法,否则它会一直阻塞):
+ (void)lowCostSemaphoreWait:(NSTimeInterval)seconds
{
// Use a semaphore to set up a low cost (non-polling) delay on whatever thread we are currently running
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC);
dispatch_after(delayTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_signal(semaphore);
});
NSLog(@"DELAYING...");
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"END of delay\n\n");
}
+ (void)repeatedlyBlockTheMainThread
{
dispatch_async(dispatch_get_main_queue(), ^{
// Block the main thread temporarily using a semaphore
[MyDebugUtilities lowCostSemaphoreWait:3.0];
// Queue up another blocking attempt to happen shortly
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC);
dispatch_after(delayTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[MyDebugUtilities repeatedlyBlockTheMainThread];
});
});
}
在我的tableView:didSelectRowAtIndexPath: 中进行 NSLog 调用,然后尝试在主线程被阻塞的某个时间段内双击确实重现了上面观察到的错误 - 按下使应用程序崩溃,并从 NSLog很明显tableView:didSelectRowAtIndexPath: 被调用了两次。我认为这里发生的情况是,当主线程被阻塞时,触摸正在排队,然后在它被释放后立即交付。在主线程未被阻塞时双击不会不产生两次对tableView:didSelectRowAtIndexPath:的调用,大概是因为它已经处理了第一次触摸并处理了推送。
因为这需要临时阻塞主线程,而在精心设计的应用程序中,这应该很少发生(如果有的话),这可以解释这样一个事实,即这真的很难重现 - 我有崩溃报告这来自一小部分用户,而且由于崩溃报告甚至没有指出我的哪个视图控制器触发了该场景,所以在我亲身体验之前,我不知道从哪里开始寻找。
要解决这个问题,真正简单的解决方案是创建一个 BOOL 属性(例如selectionAlreadyChosen),您在viewWillAppear: 中将其设置为 NO(因此当您返回此屏幕时它会被重置),然后实现tableView:willSelectRowAtIndexPath: 做类似的事情:
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (self.selectionAlreadyChosen) {
NSLog(@"BLOCKING duplicate calls to tableView:didSelectRowAtIndexPath:");
return nil;
}
self.selectionAlreadyChosen = YES;
return indexPath;
}
完成后不要忘记删除任何 NSLog 和对重复 BlockTheMainThead 的调用。
这个确切的场景可能不是其他人都遇到的问题,但我认为 Apple 处理同时多个表格视图选择的方式存在问题 - 它不仅仅是双击 - 通过疯狂点击来测试上述解决方案当主线程被阻塞时,同一个单元格生成BLOCKING duplicate calls to tableView:didSelectRowAtIndexPath:消息流。
实际上,在 iOS 6 上对此进行测试 - 多次调用 tableView:didSelectRowAtIndexPath: 并没有被阻止,但处理方式不同(这当然是反复调用 pushViewController: 的结果!) - 在 iOS 6 上多次点击,而主线程被阻塞会给你消息:
警告:尝试从视图控制器中关闭
在演示或解雇时
正在进行中!
就在被毫不客气地甩出 UINavigationController 之前。因此,这可能不是 iOS 7 的错误,而是处理方式发生了变化,并且一直存在的问题现在表现为崩溃。
更新:
通过我的项目寻找其他存在此问题的 tableView,我发现了一个没有。我能看到的唯一区别是这个工作是可编辑的(因此支持切换编辑模式、移动行和滑动删除),并且更接近我的视图层次结构的根。上面失败的那个是不可编辑的,并且位于视图层次结构的下方——出现在许多模态序列之后。我看不出它们之间有任何其他主要区别——它们都有一个共同的超类——但是一个人在没有上述修复的情况下反复尝试pushViewController:,而另一个人则没有。
有趣的是,没有出现问题的那个在编辑模式下也会调用performSegueWithIdentifier:而不是pushViewController:,在这种情况下,它会重复调用tableView:didSelectRowAtIndexPath:(因此@987654343 @) - 但是调用pushViewController: 似乎取消了对tableView:didSelectRowAtIndexPath: 的重复调用。
困惑?我知道我是。试图通过使其不可编辑来破坏“工作”视图控制器什么都不做。将非编辑模式pushViewController: 替换为performSegueWithIdentifier: 之一会导致segue 被多次调用,因此与tableView 中的编辑模式无关。
将层次结构中的工作视图控制器替换为不工作的视图控制器(以便它通过 rootViewController 关系而不是模式 segues 链接)修复了以前损坏的视图控制器(使用以前的修复从中取出)。
结论 - 关于您的视图层次结构是如何连接的,可能是使用故事板或模态序列,或者我的视图层次结构损坏导致这种情况 - 导致多个如果您的主线程恰好在用户双击时被阻塞,则点击要立即发送的表格视图单元格。如果您在tableView:didSelectRowAtIndexPath: 中调用pushViewController:,它将最终被多次调用。在 iOS 6 上,这会将你从有问题的视图控制器中删除。在 iOS 7 上,这将导致崩溃。