最近我们项目线上出现了几个Swift代码的crash问题,和大家一起分析一下问题的原因。
首先看代码:
class DEFContactDetailViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
deinit {
tableView.delegate = nil
tableView.dataSource = nil
}
lazy var tableView = UITableView(frame: view.bounds, style: .plain).then {
$0.backgroundColor = UIColor.white
$0.estimatedRowHeight = 44
$0.rowHeight = UITableViewAutomaticDimension
$0.dataSource = self
$0.delegate = self
$0.tableFooterView = UIView()
}
override func viewDidLoad() {
super.viewDidLoad()
title = "联系人"
view.addSubview(tableView)
tableView.mas_makeConstraints { maker in
maker?.edges.equalTo()(view)
}
//....
}
}
复制代码
这是一个联系人详情页面,主要有一个UITableView
来展示内容,只不过tableView是一个lazy
属性,在构造tableView时设置它的一些属性,并绑定了dataSource
和delegate
。另外在deinit
方法中将dataSource
和delegate
置空。
这里有人可能要说deinit
方法中的事情其实没必要做了,因为现在UITableView
的delegate
和dataSource
已经是weak
的了。没错,但是delegate
和dataSource
在iOS9以后才是weak
,在iOS8以前还是assign
的,这里在deinit
中手动将其置为nil
也是为了兼容iOS8的系统。
如果在某种情况下创建一个TestController的实例,但是没等TestController的view显示出来实例就被释放的话上面的代码就会crash。
let controller = DEFContactDetailViewController()
if (/*hasNavigationController*/) {
//push controller
}
复制代码
如果上面的if条件不成立的话相当于这样:
let controller = DEFContactDetailViewController()
controller = nil
复制代码
这种情况下发生crash的原因会是一下两种情况:
-
原因1: 是因为如果执行deinit时tableView是nil的话,deinit中的代码实际上相当于懒加载调用tableView的初始化方法,在初始化方法中设置了delegate和dataSource,之就相当于OC中在dealloc中访问weak的self一样会crash。
-
原因2:如果这时controller的view还没构造出来的话view属性这时还是nil,上面的 let tableView = UITableView(frame: view.bounds, style: .plain)这一行中访问了view.bounds,这就相当于对view属性做强制解包。可想而知对nil强制解包的后果,自然是crash。
解决方案:
-
将deinit中
tableView.delegate = nil tableView.dataSource = nil
这两行删掉,当然这样也就抛弃了对iOS8的兼容 -
不用lazy,将tableView的构造逻辑直接写在viewDidLoad方法中
-
希望Swift中能够像OC那样只访问实例而不会触发lazy调用的访问方式,
- (void)dealloc {
_tableView.delegate = nil;
_tableView.dataSource = nil;
}
复制代码
对于这种lazy的用法我相信在大家的项目中应该也会有,只不过没有遇到我这种会出crash的场景。可惜目前Swift并没有第三点中提到的这个和OC类似的特性。所以大家在用lazy的时候特别是类似UITableView这类的对象一定要特别小心了,避免掉进这个陷阱中去。