UITableViewCell复用机制及踩坑总结

6,644 阅读4分钟

最近在项目开发中遇到了一些关于UITableViewCell的问题,当我在反复滑动tableView的时候,cell上的内容出现了变动,同时我最近也刚好在掘金上看到了另外一篇关于这个问题的文章,参考了一下并稍作深入的研究了一下UITableViewCell的复用机制,如有问题还请指正,一起提高。

关于这个复用机制,网上众说纷纭,其中大部分的说法都是先生成能覆盖一屏幕的cell,然后上下滑动的时候再把这些cell复用,比如我向下滑的时候最上面的cell移除了视图,那么就把他复用为最下面的cell。这样只需生成略多于一屏幕的cell数量就能实现功能,还节省资源。

不过UITableView毕竟没有开源,具体怎么实现的也不清楚,所以搞了一个小小的实验,直接贴一段代码。
编译环境:Xcode11.3.1、iOS13.2

//TableViewController.swift
class TableViewController: UITableViewController {
    
    let dataSourse = ["","","","","test","test","test","test","test","test","test","test"]

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.register(TestTableViewCell.self, forCellReuseIdentifier: TestTableViewCell.reuseIdentifier)
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        var cell = tableView.dequeueReusableCell(withIdentifier: TestTableViewCell.reuseIdentifier) as? TestTableViewCell

        print("--------cell" + String(indexPath.row) + "被添加到视图中了")

        cell!.data = dataSourse[indexPath.row]
        
        print(cell!.description)
        
        return cell!
    }
    
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 100
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSourse.count
    }
}
//TestTableViewCell.swift
class TestTableViewCell: UITableViewCell {
    
    static let reuseIdentifier = "TestTableViewCell"
    
    let indicateLabel = UILabel()
    let dataLabel = UILabel()
    
    var data: String! {
        didSet {
            display()
        }
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        
        indicateLabel.frame = CGRect(x: 0, y: 30, width: 100, height: 40)
        dataLabel.frame = CGRect(x: 100, y: 30, width: 100, height: 40)
        
        contentView.addSubview(indicateLabel)
        contentView.addSubview(dataLabel)
    }
    
    func display() {
        
        //需注意的是在data为空的情况下并没有对dataLabel进行赋值
        if data == "" {
            indicateLabel.text = "暂无数据"
        } else {
            indicateLabel.text = "有数据"
            dataLabel.text = data
        }
    }

}

打印结果截图如下。

我发现所有的12个cell都生成了,我特地把高度拉的很高便于测试,事实上图中仅能看见8-9个cell。

这说明了在tableView调用该数据源方法时是将所有cell全部加载好了,对于这个打印结果,我其实也半信半疑,可能是刚好某种情况让我撞见了?所以还请各位大神们指点一二(手动抱拳

那么随着我向下拉视图,可以发现cell的复用状况如下:

9、10、11的第一次出现是承接了上图,那么9、10、11的第二次出现便是下拉后的输出结果,可以明显的发现,这个新的9的cell是和第一次的11的地址是一样的,就可以理解为向下滑露出的第一个cell的内容是复用自最初加载的所有的cell的最后一个,因为此时最上面的cell还不一定消失,暂时不能被复用。那么新的10、11就和上图中的0、1对应,是复用自他们的,可以保证最上面的cell已经消失,不再能够被看到。

此时再向上滑动,可以发现复用状况更新如下:

很好理解,从下向上2、1、0按顺序出现在屏幕上,对照上面两图可以发现,这个2是没被动过的,而1、0是被11、10复用过的,那么就出现了一个一开始看会觉得很离谱的情况:

明明本来没有数据的,为什么又有了呢?关于这种情况出现的原因和解决方案,可以看这篇文章,也是最近发布的一篇,大意为复用的cell的内容设置不会更改,当我没有显式的指定某个控件的内容,再进行复用就容易出问题。本例中11、10地址的cell的右侧label值被设置为了test,而再复用到1、0中时由于代码并没有显式的给右侧label赋值(无数据),就出现了这种结果。那篇文章提供了几个解决方案及其优劣讨论,我想补充一点的就是cell无论处于哪种case都可以显式的给控件上一个值,这样就避免了这种问题的出现。

ok,踩了个小坑,也探究了cell复用到底是怎么回事。对于这个cell复用,我还是对我的实验结果不够自信。。。分享一下,希望大佬们多提宝贵意见。


更新一下,当我把cell数量增到1000的时候,发现仅仅打印到cell18,由此可以推测出之前并不是把所有的cell全部加载好放在内存中,复用池数量维持在当前屏幕可展示数量的两倍的状态,如果cell总数量小于复用池数量的话就全部加载。接下来的过程和之前一样,如果下拉的话是先把复用池中最后一个取出拿来复用,也就是18,然后是0、1、2这样。