问题描述及复现
最近一直在解决firebase上的一个列表奔溃问题,由于自己一直没有重现这个闪退, 就是一直任凭感觉的在修复,很遗憾这个奔溃问题一直都没有被解决,而且奔溃次数还不少。。。心想真是见了鬼 觉得自己代码看上去没啥问题啊,而且一直无法复现闪退的问题。
崩溃信息
花费了很多时间,一番捯饬终于复现了这个列表闪退,简单描述一下这个列表的业务逻辑,就是一个普通的商品信息的列表(tableview),可以按条件筛选对应的列表数据。复现这个问题的操作步骤是,进入列表后滑动列表数据并上拉加载出更多的商品信息,然后按条件筛选商品数据,当筛选出的商品数据很少的时候(商品数量小于筛选前的商品数量),这个时候就会发生闪退。终于找到问题出在哪儿了!也算是离解决进了一步。先前没有复现问题,是因为没有点击筛选条件去刷新列表数据。
查找bug的原因
于是,开始检查列表刷新之后的代码逻辑,下面是点击筛选条件后,商品数据请求的回调代码片段
if result.success{
self.shopListView.listTableView.tips_hidden()
if let merchantModel = result.data as? MerchantListDataModel{
if merchantModel.list.count == 0 && self.page == 1{
self.listDataArr.removeAll()
self.shopListView.endLoad()
self.shopListView.listTableView.tips_show(tips: MallViewTips.noContent, handler: {})
}else{
if self.page == 1{
self.listDataArr.removeAll()
}
self.listDataArr += merchantModel.list
if merchantModel.list.count < 10{
self.shopListView.endLoad()
}else{
self.shopListView.endRefresh()
}
}
//让列表滚动到头部
if self.page == 1{
self.shopListView.scrollToViewTop()
}
// 刷新数据源
self.listVM?.updateListData(arr: self.listDataArr, modelType: MerchantListType(rawValue: merchantModel.templateType)!)
self.shopListView.bindListDataVM(vm: self.listVM!)
}
}
经过一翻断点的调试,发现问题出现在让列表滚动到头部和刷新数据源着两个方法上。
首先看一下让列表滚动到头部这个方法具体做了什么操作,这个方法里是通过改变tableview的contentOffset的值,使tableview滚动到顶部
func scrollToViewTop() {
if self.merchantDataArr.count > 0{
listTableView.contentOffset = .zero
}
}
再看一下刷新数据源具体又做了什么,这个方法里是更新了商品列表的本地数据源,然后在刷新tableview的数据
func bindListDataVM(vm: MerchantListViewModel) {
merchantType = vm.listCellType
merchantDataArr = vm.listArr
listVM = vm
industryCateId = vm.industryCateId
DispatchQueue.main.async {
self.listTableView.reloadData()
}
}
乍一看这个代码逻辑也没有什么问题,那为什么会产生崩溃呢?原因出在异步线程执行上面,首先要明确一个问题,所有刷新UI的操作都是在主线程中执行的,而数据请求完成的回调是异步线程。
那么接下来看问题所在,首先当执行到listTableView.contentOffset = .zero这句代码时,这其实是在刷新UI,所以这句代码的执行是在主线程中进行的,而这句代码会让tableview执行奔溃日志里断点到的代理方法 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath),而此时数据请求的回调里可能已经执行完更新本地商品数据源的代码,新的商品数据源是比tableview的numberOfRows的数量少,就会数组越界,导致奔溃。
解决办法
将列表滚动到顶部的操作放在刷新完tableview的数据之后来做,见代码
func bindListDataVM(vm: MerchantListViewModel) {
merchantType = vm.listCellType
merchantDataArr = vm.listArr
listVM = vm
industryCateId = vm.industryCateId
DispatchQueue.main.async {
self.listTableView.reloadData()
if vm.scrollToTop == true, self.listTableView.numberOfRows(inSection: 0) > 0{
self.listTableView.scrollToRow(at: IndexPath.init(row: 0, section: 0), at: .none, animated: true)
}
}
}
反思
这次问题主要在于自己没有真正明白什么是异步线程之间的执行关系是无法保证谁先谁后的。还有就是对基础的知识掌握不牢固,比如listTableView.contentOffset = .zero这句代码会执行tableview的代理方法func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath),这一点先前其实并没有很清楚。所以还是要多掌握一下基础知识。
网友评论