使用swift懶加載的注意事項(xiàng)

修改老代碼后,發(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)一些看起來(lái)莫名其妙的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)一眼就看出了問題所在,那么就沒有必要看下去了。如果你沒有看出來(lái),也不要著急,這個(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):

  1. lazy var懶加載不是線程安全的
  2. 在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輸出如下:

image.png

在調(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)用過(guò)了,在self.navigationController?.pushViewController(testVC, animated: true)方法調(diào)用后,viewDidLoad不會(huì)再被調(diào)用,所以A是沒有注冊(cè)cell的。

運(yùn)行到這時(shí),控制器持有了A,而控制器的view通過(guò)addSubview持有了它的子視圖B,圖示如下:


image.png

其中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。
    // 代理方法傳遞過(guò)來(lái)的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而是使用代理方法傳遞過(guò)來(lái)的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è)屬性。

其次,在編碼過(guò)程中,要注意權(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è)人主頁(yè)領(lǐng)取哦。

作者:星的天空

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容