UITableView的Cell复用原理和源码分析

来源:zongmumask 

www.jianshu.com/p/5b0e1ca9b673


简介


在我们的日常开发中,绝大多数情况下只要详细阅读类头文件里的注释,组合UIKit框架里的大量控件就能很好的满足工作的需求。但仅仅会使用UIKit里的控件还远远不够,假如现在产品需要一个类似 Excel 样式的控件来呈现数据,需要这个控件能上下左右滑动,这时候你会发现UIKit里就没有现成的控件可用了。UITableView 可以看做一个只可以上下滚动的 Excel,所以我们的直觉是应该仿写 UITableView 来实现这个自定义的控件。这篇文章我将会通过开源项目 Chameleon 来分析UITableView的 hacking 源码,阅读完这篇文章后你将会了解 UITableView 的绘制过程和 UITableViewCell 的复用原理。 并且我会在下一篇文章中实现一个类似 Excel 的自定义控件。


Chameleon


Chameleon 是一个移植 iOS 的 UIKit 框架到 Mac OS X 下的开源项目。该项目的目的在于尽可能给出 UIKit 的可替代方案,并且让 Mac OS 的开发者尽可能的开发出类似 iOS 的 UI 界面。


UITableView的简单使用


UITableView的Cell复用原理和源码分析

创建UITableView实例对象


UITableView *tableView = [[UITableView alloc] initWithFrame:frame style:UITableViewStyleGrouped];


initWithFrame: style: 方法源码如下:


UITableView的Cell复用原理和源码分析


我将需要关注的地方做了详细的注释,这里我们需要关注_cachedCells, _sections, _reusableCells 这三个变量的作用。


设置数据源


tableView.dataSource = self;


下面是 dataSrouce 的 setter 方法源码:


(void)setDataSource:(id)newSource

{

    _dataSource = newSource;

 

    _dataSourceHas.numberOfSectionsInTableView = [_dataSourcerespondsToSelector:@selector(numberOfSectionsInTableView:)];

    _dataSourceHas.titleForHeaderInSection = [_dataSourcerespondsToSelector:@selector(tableView:titleForHeaderInSection:)];

    _dataSourceHas.titleForFooterInSection = [_dataSourcerespondsToSelector:@selector(tableView:titleForFooterInSection:)];

    _dataSourceHas.commitEditingStyle = [_dataSourcerespondsToSelector:@selector(tableView:commitEditingStyle:forRowAtIndexPath:)];

    _dataSourceHas.canEditRowAtIndexPath = [_dataSourcerespondsToSelector:@selector(tableView:canEditRowAtIndexPath:)];

 

    [self _setNeedsReload];

}


_dataSourceHas 是用于记录该数据源实现了哪些协议方法的结构体,该结构体源码如下:


struct {

        unsigned numberOfSectionsInTableView 1;

        unsigned titleForHeaderInSection 1;

        unsigned titleForFooterInSection 1;

        unsigned commitEditingStyle 1;

        unsigned canEditRowAtIndexPath 1;

    } _dataSourceHas;


记录是否实现了某协议可以使用布尔值来表示,布尔变量占用的内存大小一般为一个字节,即8比特。但该结构体使用了 bitfields 用一个比特(0或1)来记录是否实现了某协议,大大缩小了占用的内存。

在设置好了数据源后需要打一个标记,告诉NSRunLoop数据源已经设置好了,需要在下一次循环中使用数据源进行布局。下面看看 _setNeedReload 的源码:


(void)_setNeedsReload

{

    _needsReload = YES;

    [self setNeedsLayout];

}


在调用了 setNeedsLayout 方法后,NSRunloop 会在下一次循环中自动调用 layoutSubViews 方法。


  • 视图的内容需要重绘时可以调用 setNeedsDisplay 方法,该方法会设置该视图的 displayIfNeeded 变量为 YES ,NSRunLoop 在下一次循环检中测到该值为 YES 则会自动调用 drawRect 进行重绘。

  • 视图的内容没有变化,但在父视图中位置变化了可以调用 setNeedsLayout,该方法会设置该视图的 layoutIfNeeded 变量为YES,NSRunLoop 在下一次循环检中测到该值为 YES 则会自动调用 layoutSubViews 进行重绘。

  • 更详细的内容可参考 When is layoutSubviews called?

    http://stackoverflow.com/questions/728372/when-is-layoutsubviews-called

设置代理


tableView.delegate = self;


下面是 delegate 的 setter 方法源码:


(void)setDelegate:(id)newDelegate

{

    [super setDelegate:newDelegate];

    _delegateHas.heightForRowAtIndexPath = [newDelegaterespondsToSelector:@selector(tableView:heightForRowAtIndexPath:)];

    _delegateHas.heightForHeaderInSection = [newDelegaterespondsToSelector:@selector(tableView:heightForHeaderInSection:)];

    _delegateHas.heightForFooterInSection = [newDelegaterespondsToSelector:@selector(tableView:heightForFooterInSection:)];

    _delegateHas.viewForHeaderInSection = [newDelegaterespondsToSelector:@selector(tableView:viewForHeaderInSection:)];

    _delegateHas.viewForFooterInSection = [newDelegaterespondsToSelector:@selector(tableView:viewForFooterInSection:)];

    _delegateHas.willSelectRowAtIndexPath = [newDelegaterespondsToSelector:@selector(tableView:willSelectRowAtIndexPath:)];

    _delegateHas.didSelectRowAtIndexPath = [newDelegaterespondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)];

    _delegateHas.willDeselectRowAtIndexPath = [newDelegaterespondsToSelector:@selector(tableView:willDeselectRowAtIndexPath:)];

    _delegateHas.didDeselectRowAtIndexPath = [newDelegaterespondsToSelector:@selector(tableView:didDeselectRowAtIndexPath:)];

    _delegateHas.willBeginEditingRowAtIndexPath = [newDelegaterespondsToSelector:@selector(tableView:willBeginEditingRowAtIndexPath:)];

    _delegateHas.didEndEditingRowAtIndexPath = [newDelegaterespondsToSelector:@selector(tableView:didEndEditingRowAtIndexPath:)];

    _delegateHas.titleForDeleteConfirmationButtonForRowAtIndexPath = [newDelegaterespondsToSelector:@selector(tableView:titleForDeleteConfirmationButtonForRowAtIndexPath:)];

}


与设置数据源一样,这里使用了类似的结构体来记录代理实现了哪些协议方法。


UITableView绘制


由于在设置数据源中调用了 setNeedsLayout 方法打上了需要布局的 flag,所以会在 1/60 秒(NSRunLoop的循环周期)后自动调用 layoutSubViews。layoutSubViews 的源码如下:


(void)layoutSubviews

{

    //对子视图进行布局,该方法会在第一次设置数据源调用 setNeedsLayout 方法后自动调用。

    //并且 UITableView 是继承自 UIScrollview ,当滚动时也会触发该方法的调用

    _backgroundView.frame = self.bounds;

 

    //在进行布局前必须确保 section 已经缓存了所有高度相关的信息

    [self _reloadDataIfNeeded];    

 

    //对 UITableView 的 section 进行布局,包含 section 的头部,尾部,每一行 Cell

    [self _layoutTableView];

 

    //对 UITableView 的头视图,尾视图进行布局

    [super layoutSubviews];

}


需要注意的是由于 UITableView 是继承于 UIScrollView,所以在 UITableView 滚动时会自动调用该方法,详细内容可以参考 When is layoutSubviews called?

http://stackoverflow.com/questions/728372/when-is-layoutsubviews-called


下面依次来看三个主要方法的实现。

_reloadDataIfNeeded 的源码如下:


UITableView的Cell复用原理和源码分析

其中 _updateSectionsCashe 方法是最重要的,该方法在数据源更新后至下一次数据源更新期间只能调用一次,该方法的源码如下:


UITableView的Cell复用原理和源码分析

UITableView的Cell复用原理和源码分析

我在需要注意的地方加了注释,上面方法主要是记录每个 Cell 的高度和整个 section 的高度,并把结果同过 UITableViewSection 对象缓存起来。


_layoutTableView 的源码实现如下:


UITableView的Cell复用原理和源码分析
UITableView的Cell复用原理和源码分析
UITableView的Cell复用原理和源码分析


关于 UIView 的 frame 和bounds 的区别可以参考 What’s the difference between the frame and the bounds?

http://stackoverflow.com/questions/1210047/cocoa-whats-the-difference-between-the-frame-and-the-bounds


这里使用了三个容器 _cachedCells, availableCells, _reusableCells 完成了 Cell 的复用,这是 UITableView 最核心的地方。

下面一起看看三个容器在创建到滚动整个过程中所包含的元素的变化情况。

在第一次设置了数据源调用该方法时,三个容器的内容都为空,在调用完该方法后 _cachedCells 包含了当前所有可视 Cell 与其对应的indexPath 的键值对,availableCells 与 _reusableCells 仍然为空。只有在滚动起来后 _reusableCells 中才会出现多余的未显示可复用的 Cell。


  • 刚创建 UITableView 时的状态如下图(红色为屏幕内容即可视区域,蓝色为超出屏幕的内容,即不可视区域):


UITableView的Cell复用原理和源码分析


如图,当前 _cachedCells 的元素为当前可视的所有 Cell 与其对应的 indexPath 的键值对。


  • 向上滚动一个 Cell 的过程中,由于 availableCells 为 _cachedCells 的拷贝,所以可根据 indexPath 直接取到对应的 Cell,这时从底部滚上来的第7行,由于之前的 _reusableCells 为空,所以该 Cell 是直接创建的而并非复用的,由于顶部 Cell 滚动出了可视区域,所以被加入了 _reusableCells 中以便后续滚动复用。滚动完一行后的状态变为了 _cachedCells 包含第 2 行到第 7 行 Cell 的引用,_reusableCells 包含第一行 之前滚动出可视区域的第一行 Cell 的引用。


UITableView的Cell复用原理和源码分析


  • 当向上滚动两个 Cell 的过程中,同理第 3 行到第 7 行的 Cell 可以通过对应的 indexPath 从 _cachedCells 中获取。这时 _reusableCells 中正好有一个可以复用的 Cell 用来从底部滚动上来的第 8 行。滚动出顶部的第 2 行 Cell 被加入 _reusableCells 中。


UITableView的Cell复用原理和源码分析


总结


到此你已经了解了 UITableView 的 Cell 的复用原理,可以根据需要定制出更复杂的控件。

本文内容的相关公众号推荐

程序员共读

UITableView的Cell复用原理和源码分析

Java编程精选↓

UITableView的Cell复用原理和源码分析

更多推荐年薪百万的程序员都在干什么?

涵盖:程序人生、算法与数据结构、黑客技术与网络安全、大数据技术、前端开发、Java、Python、Web开发、安卓开发、iOS开发、C/C++、.NET、Linux、数据库、运维等