修改老代碼后缰猴,發(fā)現(xiàn)UITableView會(huì)在創(chuàng)建cell時(shí)閃退体啰,原因是在調(diào)用dequeueReusableCell(withIdentifier:)創(chuàng)建cell時(shí)返回了nil莹菱。但是檢查代碼掏湾,確認(rèn)在viewDidLoad注冊(cè)了這個(gè)cell攒发,按道理不應(yīng)該返回nil有滑。后面分析才發(fā)現(xiàn),由于lazy var不是線程安全的晋修,在碰到viewDidLoad的某個(gè)特殊調(diào)用時(shí)機(jī)時(shí)就會(huì)出現(xiàn)這個(gè)問題吧碾,而且代碼可能在大部分場(chǎng)景正常運(yùn)行凰盔,然后出現(xiàn)一些看起來莫名其妙的bug墓卦!
iOS學(xué)習(xí)資料可關(guān)注個(gè)人資料領(lǐng)取
樣例
我把問題代碼簡(jiǎn)化后如下:
class TestTableViewController: UIViewController {
/// 使用懶加載創(chuàng)建tableView
lazy var tableView: UITableView = {
print("start init testLabel, isViewLoaded \(self.isViewLoaded)")
let tableView = UITableView.init(frame: self.view.bounds)
print("created tableView \(tableView)")
tableView.delegate = self
tableView.dataSource = self
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
print(#function)
view.addSubview(tableView)
print(#function, "tableView \(tableView) register cell")
// 注冊(cè)cell
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
}
extension TestTableViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")!
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
}
// 調(diào)用方式如下
@IBAction func showTestTableViewVC(_ sender: Any) {
let testVC = TestTableViewController.init()
// 引起問題的關(guān)鍵代碼
testVC.tableView.isScrollEnabled = false
self.navigationController?.pushViewController(testVC, animated: true)
}
如果你已經(jīng)一眼就看出了問題所在,那么就沒有必要看下去了户敬。如果你沒有看出來落剪,也不要著急,這個(gè)問題確實(shí)挺隱蔽的尿庐。上述代碼運(yùn)行后忠怖,會(huì)出現(xiàn)報(bào)錯(cuò):TestTableViewController.swift:29: Fatal error: Unexpectedly found nil while unwrapping an Optional value。那么這個(gè)問題是怎么產(chǎn)生的類抄瑟?
問題是怎么產(chǎn)生的凡泣?
首先我們要清楚兩個(gè)知識(shí)點(diǎn):
- lazy var懶加載不是線程安全的
- 在UIViewController中,成員變量view沒有初始化及viewDidLoad方法被調(diào)用之前皮假,只要調(diào)用了成員變量view鞋拟,就會(huì)立即初始化view并調(diào)用viewDidLoad方法。
第二點(diǎn)有點(diǎn)隱蔽惹资,例如在viewDidLoad方法調(diào)用之前調(diào)用self.view.bounds就會(huì)觸發(fā)贺纲。
上述代碼運(yùn)行后的Log輸出如下:
在調(diào)用let testVC = TestTableViewController.init()初始化控制器后,我們立即調(diào)用了testVC.tableView.isScrollEnabled = false褪测,這個(gè)時(shí)候會(huì)進(jìn)入tableView的懶加載部分:
lazy var tableView: UITableView = {
print("start init testLabel, isViewLoaded \(self.isViewLoaded)")
// 注意猴誊,這里調(diào)用了self.view,會(huì)導(dǎo)致`viewDidLoad`被提前調(diào)用侮措!
let tableView = UITableView.init(frame: self.view.bounds)
print("created tableView \(tableView)")
tableView.delegate = self
tableView.dataSource = self
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
print(#function)
view.addSubview(tableView)
print(#function, "tableView \(tableView) register cell")
// 注冊(cè)cell
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
我們先定義這次要?jiǎng)?chuàng)建的tableView為A懈叹。這部分懶加載代碼由于錯(cuò)誤的調(diào)用了self.view,導(dǎo)致self.view初始化和viewDidLoad方法被提前調(diào)用分扎,此時(shí)成員變量tableView還沒有被初始化完成澄成,而viewDidLoad方法中又調(diào)用了tableView,由于lazy不是線程安全的,所以又遞歸進(jìn)入了上述初始化tableView的邏輯环揽,這個(gè)時(shí)候self.view已經(jīng)被創(chuàng)建了略荡,所以會(huì)初始化完成,我們定義這次創(chuàng)建的tableView為B歉胶,這個(gè)時(shí)候控制器持有的tableView對(duì)象是B汛兜,它會(huì)在viewDidLoad方法的這次調(diào)用中注冊(cè)cell。
上述邏輯跑完后通今,A才緊隨其后完成創(chuàng)建粥谬,并替換B成為控制器的新成員變量,而且由于viewDidLoad已經(jīng)被調(diào)用過了辫塌,在self.navigationController?.pushViewController(testVC, animated: true)方法調(diào)用后漏策,viewDidLoad不會(huì)再被調(diào)用,所以A是沒有注冊(cè)cell的臼氨。
運(yùn)行到這時(shí)掺喻,控制器持有了A,而控制器的view通過addSubview持有了它的子視圖B储矩,圖示如下:
其中B對(duì)象在viewDidLoad方法中注冊(cè)了cell感耙,而A對(duì)象并沒有注冊(cè),所以在代理方法中創(chuàng)建cell時(shí)返回了nil持隧,導(dǎo)致了crash即硼。如果對(duì)這部分不理解,可以多看幾遍代碼和日志屡拨,理順下調(diào)用流程只酥。
crash位置代碼如下:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// self.tableView是對(duì)象A,它并沒有注冊(cè)cell呀狼。
// 代理方法傳遞過來的tableView是對(duì)象B裂允,它注冊(cè)了cell,直接使用它則不會(huì)crash
let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")!
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
而這個(gè)問題的隱蔽性在于存在兩個(gè)UITableView對(duì)象赠潦,如果在代理方法中不使用self.tableView而是使用代理方法傳遞過來的tableView叫胖,那么程序不會(huì)crash,而且顯示正常她奥。而后續(xù)會(huì)不會(huì)出現(xiàn)奇奇怪怪的問題瓮增,就完全看你的運(yùn)氣了。
當(dāng)然這個(gè)問題埋的隱蔽性并不止于此哩俭,當(dāng)外部不調(diào)用tableView屬性時(shí)绷跑,例如不像樣例代碼那樣調(diào)用testVC.tableView.isScrollEnabled = false,那么在viewDidLoad方法中會(huì)正常執(zhí)行tableView的初始化凡资,一切都是正常的砸捏。但是一旦哪位同事在外部調(diào)用了一次谬运,那么潘多拉魔盒就打開了~
解決方案
要解決這種問題,需要我們有良好的編碼規(guī)范垦藏。首先梆暖,要強(qiáng)化lazy不是線程安全的概念,在懶加載中只做這個(gè)變量初始化的事情掂骏,盡量避免其它變量及邏輯的混入轰驳。在UIViewController及其子類的懶加載邏輯中,避免對(duì)view的調(diào)用弟灼。我看很多人喜歡在懶加載邏輯中調(diào)用view.addSubView()或view.bounds级解,這是不太對(duì)的,因?yàn)樵趇sViewLoaded為false的情況下田绑,對(duì)view的調(diào)用就代表著viewDidLoad方法的提前調(diào)用勤哗,這讓程序的邏輯變得有些混亂,除非你能保證在viewDidLoad之后調(diào)用這個(gè)屬性掩驱。
其次芒划,在編碼過程中,要注意權(quán)限的控制昙篙,設(shè)計(jì)合適的接口腊状,這樣對(duì)使用者更友好诱咏,也能規(guī)避很多異常場(chǎng)景苔可,當(dāng)然這對(duì)開發(fā)者的要求較高,需要平常多加修煉和積累了袋狞。
關(guān)于OC
另外需要注意的是焚辅,OC的懶加載也有同樣的問題。但是OC可以優(yōu)化寫法避免出現(xiàn)這個(gè)問題苟鸯,而Swift不行同蜻。
關(guān)鍵代碼如下:
- (UITableView *)tableView {
if (!_tableView) {
// 第一種用法:這樣調(diào)用會(huì)出現(xiàn)異常
// _tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
// 第二種用法:這樣是正常的
_tableView = [[UITableView alloc] init];
_tableView.frame = self.view.bounds;
_tableView.delegate = self;
_tableView.dataSource = self;
}
return _tableView;
}
上述代碼中的第二種用法不會(huì)出現(xiàn)問題,是由于在_tableView.frame = self.view.bounds;這行代碼才引入的self.view早处,此時(shí)_tableView
已經(jīng)有值湾蔓,后續(xù)代碼不會(huì)執(zhí)行。
雖然沒有問題砌梆,但是不推薦這樣使用默责,因?yàn)樗€是引起了viewDidLoad的提前執(zhí)行。
如果你有所收獲咸包,不如動(dòng)動(dòng)小手指頭雙擊一下桃序。
如需iOS資料包,可關(guān)注個(gè)人主頁領(lǐng)取哦烂瘫。
作者:星的天空