使用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)一些看起來莫名其妙的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):

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

運(yùn)行到這時(shí)掺喻,控制器持有了A,而控制器的view通過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呀狼。
    // 代理方法傳遞過來的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)取哦烂瘫。

作者:星的天空

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末媒熊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌芦鳍,老刑警劉巖嚷往,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異柠衅,居然都是意外死亡间影,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門茄茁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來魂贬,“玉大人,你說我怎么就攤上這事裙顽「对铮” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵愈犹,是天一觀的道長(zhǎng)键科。 經(jīng)常有香客問我,道長(zhǎng)漩怎,這世上最難降的妖魔是什么勋颖? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮勋锤,結(jié)果婚禮上饭玲,老公的妹妹穿的比我還像新娘。我一直安慰自己叁执,他們只是感情好茄厘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谈宛,像睡著了一般次哈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吆录,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天窑滞,我揣著相機(jī)與錄音,去河邊找鬼恢筝。 笑死哀卫,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的滋恬。 我是一名探鬼主播聊训,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼恢氯!你這毒婦竟也來了带斑?” 一聲冷哼從身側(cè)響起鼓寺,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎勋磕,沒想到半個(gè)月后妈候,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡挂滓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年苦银,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赶站。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡幔虏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贝椿,到底是詐尸還是另有隱情想括,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布烙博,位于F島的核電站瑟蜈,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏渣窜。R本人自食惡果不足惜铺根,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望乔宿。 院中可真熱鬧位迂,春花似錦、人聲如沸予颤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛤虐。三九已至,卻和暖如春肝陪,著一層夾襖步出監(jiān)牢的瞬間驳庭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工氯窍, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饲常,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓狼讨,卻偏偏與公主長(zhǎng)得像贝淤,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子政供,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容