使用swift懒加载的注意事项

修改老代码后,发现UITableView会在创建cell时闪退,原因是在调用dequeueReusableCell(withIdentifier:)创建cell时返回了nil。但是检查代码,确认在viewDidLoad注册了这个cell,按道理不应该返回nil。后面分析才发现,由于lazy var不是线程安全的,在碰到viewDidLoad的某个特殊调用时机时就会出现这个问题,而且代码可能在大部分场景正常运行,然后出现一些看起来莫名其妙的bug!

iOS学习资料可关注个人资料领取

样例

我把问题代码简化后如下:

class TestTableViewController: UIViewController {
    /// 使用懒加载创建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")
        // 注册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
    }
}

// 调用方式如下
@IBAction func showTestTableViewVC(_ sender: Any) {
    let testVC = TestTableViewController.init()
    // 引起问题的关键代码
    testVC.tableView.isScrollEnabled = false
    self.navigationController?.pushViewController(testVC, animated: true)
}

如果你已经一眼就看出了问题所在,那么就没有必要看下去了。如果你没有看出来,也不要着急,这个问题确实挺隐蔽的。上述代码运行后,会出现报错:TestTableViewController.swift:29: Fatal error: Unexpectedly found nil while unwrapping an Optional value。那么这个问题是怎么产生的类?

问题是怎么产生的?

首先我们要清楚两个知识点:

  1. lazy var懒加载不是线程安全的
  2. 在UIViewController中,成员变量view没有初始化及viewDidLoad方法被调用之前,只要调用了成员变量view,就会立即初始化view并调用viewDidLoad方法。

第二点有点隐蔽,例如在viewDidLoad方法调用之前调用self.view.bounds就会触发。

上述代码运行后的Log输出如下:

image.png

在调用let testVC = TestTableViewController.init()初始化控制器后,我们立即调用了testVC.tableView.isScrollEnabled = false,这个时候会进入tableView的懒加载部分:

lazy var tableView: UITableView = {
    print("start init testLabel, isViewLoaded \(self.isViewLoaded)")
    // 注意,这里调用了self.view,会导致`viewDidLoad`被提前调用!
    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")
    // 注册cell
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}

我们先定义这次要创建的tableView为A。这部分懒加载代码由于错误的调用了self.view,导致self.view初始化和viewDidLoad方法被提前调用,此时成员变量tableView还没有被初始化完成,而viewDidLoad方法中又调用了tableView,由于lazy不是线程安全的,所以又递归进入了上述初始化tableView的逻辑,这个时候self.view已经被创建了,所以会初始化完成,我们定义这次创建的tableView为B,这个时候控制器持有的tableView对象是B,它会在viewDidLoad方法的这次调用中注册cell。
上述逻辑跑完后,A才紧随其后完成创建,并替换B成为控制器的新成员变量,而且由于viewDidLoad已经被调用过了,在self.navigationController?.pushViewController(testVC, animated: true)方法调用后,viewDidLoad不会再被调用,所以A是没有注册cell的。

运行到这时,控制器持有了A,而控制器的view通过addSubview持有了它的子视图B,图示如下:


image.png

其中B对象在viewDidLoad方法中注册了cell,而A对象并没有注册,所以在代理方法中创建cell时返回了nil,导致了crash。如果对这部分不理解,可以多看几遍代码和日志,理顺下调用流程。

crash位置代码如下:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // self.tableView是对象A,它并没有注册cell。
    // 代理方法传递过来的tableView是对象B,它注册了cell,直接使用它则不会crash
    let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")!
    cell.textLabel?.text = "\(indexPath.row)"
    return cell
}

而这个问题的隐蔽性在于存在两个UITableView对象,如果在代理方法中不使用self.tableView而是使用代理方法传递过来的tableView,那么程序不会crash,而且显示正常。而后续会不会出现奇奇怪怪的问题,就完全看你的运气了。

当然这个问题埋的隐蔽性并不止于此,当外部不调用tableView属性时,例如不像样例代码那样调用testVC.tableView.isScrollEnabled = false,那么在viewDidLoad方法中会正常执行tableView的初始化,一切都是正常的。但是一旦哪位同事在外部调用了一次,那么潘多拉魔盒就打开了~

解决方案

要解决这种问题,需要我们有良好的编码规范。首先,要强化lazy不是线程安全的概念,在懒加载中只做这个变量初始化的事情,尽量避免其它变量及逻辑的混入。在UIViewController及其子类的懒加载逻辑中,避免对view的调用。我看很多人喜欢在懒加载逻辑中调用view.addSubView()或view.bounds,这是不太对的,因为在isViewLoaded为false的情况下,对view的调用就代表着viewDidLoad方法的提前调用,这让程序的逻辑变得有些混乱,除非你能保证在viewDidLoad之后调用这个属性。

其次,在编码过程中,要注意权限的控制,设计合适的接口,这样对使用者更友好,也能规避很多异常场景,当然这对开发者的要求较高,需要平常多加修炼和积累了。

关于OC

另外需要注意的是,OC的懒加载也有同样的问题。但是OC可以优化写法避免出现这个问题,而Swift不行。

关键代码如下:

- (UITableView *)tableView {
    if (!_tableView) {
        // 第一种用法:这样调用会出现异常
//        _tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
        // 第二种用法:这样是正常的
        _tableView = [[UITableView alloc] init];
        _tableView.frame = self.view.bounds;

        _tableView.delegate = self;
        _tableView.dataSource = self;
    }
    return _tableView;
}

上述代码中的第二种用法不会出现问题,是由于在_tableView.frame = self.view.bounds;这行代码才引入的self.view,此时_tableView
已经有值,后续代码不会执行。
虽然没有问题,但是不推荐这样使用,因为它还是引起了viewDidLoad的提前执行。

如果你有所收获,不如动动小手指头双击一下。

如需iOS资料包,可关注个人主页领取哦。

作者:星的天空

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容