UITableView reloadData在iOS 11中重新出现时崩溃
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UITableView reloadData在iOS 11中重新出现时崩溃相关的知识,希望对你有一定的参考价值。
更新:在我看来,问题仍然是相关的,所以我在标记我的代码中存在潜在的设计缺陷。我在VC1的viewWillAppear:
中调用了异步数据填充方法,这绝不是填充数据和重新加载表视图的好地方,除非所有内容都在主线程中序列化。当您必须重新加载表视图时,代码中总会有潜在的执行点,而viewWillAppear
不是其中之一。从VC2返回时,我总是在VC1 viewWillAppear
中重新加载表视图数据源。但是理想的设计可以使用来自VC2的放松信号,并且只有在实际需要时才从VC2重新填充数据源(prepareForSegue
)。不幸的是,到目前为止似乎没人提到它:(
我认为之前有过类似的问题。不幸的是,他们都没有解决我面临的问题。
我的问题结构非常简单。我有两个视图控制器,比如VC1和VC2。在VC1中,我显示了一个UITableView中的一些项目列表,从数据库加载,在VC2中我显示了所选项目的详细信息,并进行编辑和保存。当用户从VC2返回VC1时,我必须重新填充数据源并重新加载表。 VC1和VC2都嵌入在UINavigationController中。
听起来非常微不足道,确实是这样,直到我在UI线程中做所有事情。问题是在VC1中加载列表有点费时。因此,我必须将繁重的数据加载任务委托给一些后台工作线程,并在数据加载完成时重新加载主线程上的表,以提供流畅的UI体验。所以我的初始构造类似于以下内容:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
dispatch_async(self.application.commonWorkerQueue, ^{
[self populateData]; //populate datasource
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData]; //reload table view
});
});
}
这个功能非常实用,直到ios10从UITableView
停止通过reloadData
立即呈现并开始将reloadData
视为注册请求,在运行循环的某些后续迭代中重新加载UITableView
。所以我发现我的应用程序开始偶尔崩溃,如果[self.tableView reloadData]
在随后调用[self populateData]
之前没有完成并且这是非常明显的,因为[self populateData]
不再是线程安全的,如果数据源在reloadData
完成之前发生变化很可能崩溃应用程序。所以我尝试添加一个信号量来使[self populateData]
线程安全,我发现它工作得很好。我后来的构造类似于以下内容:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
dispatch_async(self.application.commonWorkerQueue, ^{
[self populateData]; //populate datasource
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData]; //reload table view
dispatch_async(dispatch_get_main_queue(), ^{
dispatch_semaphore_signal(self.datasourceSyncSemaphore); //let the app know that it is free to repopulate datasource again
});
});
dispatch_semaphore_wait(self.datasourceSyncSemaphore, DISPATCH_TIME_FOREVER); //wait on a semaphore so that datasource repopulation is blocked until tableView reloading completes
});
}
不幸的是,这个结构在iOS11之后也崩溃了,当我向下滚动VC1中的UITableView
时,选择一个调出VC2然后再回到VC1的项目。它再次调用VC1的viewWillAppear:
,然后尝试通过[self populateData]
重新填充数据源。但崩溃的堆栈跟踪显示UITableView已经开始从头开始重新创建它的单元格,并且出于某种原因调用tableView:cellForRowAtIndexPath:
方法,甚至在viewWillAppear:
之前,我的数据源在后台重新填充,并且它处于某种不一致的状态。最终应用程序崩溃了。最令人惊讶的是,只有当我选择了最初不在屏幕上的底行时才会发生这种情况。以下是崩溃期间的堆栈跟踪:
我知道如果我从主线程中调用两个方法,一切都会正常运行,如下所示:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self populateData]; //populate datasource
[self.tableView reloadData]; //reload table view
}
但这不是预期的良好用户体验。我觉得这个问题发生了,因为当向下滚动时,UITableView
正试图在重新出现时获取屏幕外的顶行。但不幸的是,在理解了这么多该死的东西后,我几乎无法解决这个问题。
我真的希望这个网站的专家能够帮助我摆脱困境,或者向我展示一些方法。感谢提前加载!
PS:self.application.commonWorkerQueue
是在此上下文中在后台运行的串行调度队列。
你应该拆分你的populateData
功能。让我们举例说到fetchDatabaseRows
和populateDataWithRows
。 fetchDatabaseRows
应该在自己的线程和新的数据结构中将行检索到内存中。当IO部分完成后,你应该在UI线程中调用populateDataWithRows
(然后是reloadData
)。 populateDataWithRows
应该修改TableView使用的集合。
UIKit在主线程上运行。所有UI更新必须仅在主线程上。如果数据源的更新仅在主线程上发生,则没有竞争条件。
需要了解的重要一点是,您需要保护数据。因此,如果您使用信号量或互斥量或类似这样的任何构造总是:
- 为我索取资源。 (例如:mutex.lock())
- 做处理
- 解锁资源(例如:mutex.unlock())
事实是,因为UI线程用于UI而后台线程用于处理,所以你无法锁定共享数据源,因为你也会锁定UI线程。主线程将等待从后台线程解锁。所以这个结构很大NO-NO。这意味着当UI在主线程上使用自己的副本时,populateData()函数必须在后台创建数据副本。数据准备好后,只需将更新移动到主线程中(不需要信号量或互斥量)
dispatch_async(dispatch_get_main_queue(), ^{
//update datasource for table view here
//call reload data
});
另一件事:viewWillAppear不是进行此更新的地方。因为你有推送你的细节的导航,你可以做滑动以解雇,并在midle只是改变你的想法,并保持细节。但是,将调用vc1 viewWillAppear。 Apple应该将该方法重命名为“viewWillAppearMaybe”:)。所以正确的做法是创建一个协议,定义将被调用的方法,并使用委托只调用一次更新函数。这不会导致崩溃错误,但为什么不止一次调用更新?另外,为什么你要提取所有项目,如果只有一个已经改变?我只会更新1个项目。
还有一个:你可能正在创建参考周期。在块中使用self时要小心。
如果它看起来像这样,你的第一个例子几乎是好的:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
dispatch_async(self.application.commonWorkerQueue, ^{
NSArray* newData = [self populateData]; //this creates new array, don't touch tableView data source here!
dispatch_async(dispatch_get_main_queue(), ^{
self.tableItems = newData; //replace old array with new array
[self.tableView reloadData]; //reload
});
});
}
(self.tableItems是NSArray,tableView的简单数据源作为数据源的示例)
我的假设是因为在getMain中访问self.tableView时你有裁判周期。 Ans在背景中某处泄露了这个表视图的泄漏版本,它开始在iOS 11中崩溃应用程序
您可以使用Xcode中的内存图来验证这一点。
要修复此访问权限,您需要像这样访问自我的弱副本
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
__weak typeof(self) weakSelf = self;
dispatch_async(self.application.commonWorkerQueue, ^{
if (!weakSelf) { return; }
[weakSelf populateData]; //populate datasource
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.tableView reloadData]; //reload table view
});
});
}
在iOS11中,执行“繁重的数据加载任务”的正确方法是实现UITableViewDataSourcePrefetching协议,如下所述:https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching
如果正确实现'tableView:prefetchRowsAtIndexPaths:',则不必担心后台线程,工作队列,临时数据源或线程同步。 UIKit为您完成所有这些工作。
更新:在更深入地查看您的问题之后,您的问题的根本原因似乎是为您的tableview使用可变的支持数据结构。如果没有在数据更改的同一运行循环迭代中显式调用reloadData
,系统将期望数据永远不会更改。行总是懒洋洋地加载。
正如其他人所说,解决方案是使用具有不可变值的readwrite
属性。数据处理完成后,更新属性并在主队列上调用reloadData
。
以上是关于UITableView reloadData在iOS 11中重新出现时崩溃的主要内容,如果未能解决你的问题,请参考以下文章
iOS swift UITableView ReloadData 性能
UITableView 的 selectRowAtIndexPath:animated:scrollPosition: 在 iOS 8 中 reloadData 后不起作用
UITableView reloadData在iOS 11中重新出现时崩溃
iOS UITableView reloadData 导致键盘收起不能连续输入问题解决办法