请选择 进入手机版 | 继续访问电脑版

技术控

    今日:11| 主题:54646
收藏本版 (1)
最新软件应用技术尽在掌握

[其他] UITableView的Cell复用原理和源码分析

[复制链接]
落日夕阳 发表于 2016-11-29 18:21:20
209 1

立即注册CoLaBug.com会员,免费获得投稿人的专业资料,享用更多功能,玩转个人品牌!

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
简介

   在我们的日常开发中,绝大多数情况下只要详细阅读类头文件里的注释,组合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的简单使用

  1. //创建UITableView对象,并设置代代理和数据源为包含该视图的视图控制器
  2. UITableView *tableView = [[UITableView alloc] initWithFrame:frame style:UITableViewStyleGrouped];   
  3. tableView.delegate = self;
  4. tableView.dataSource = self;
  5. [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kReuseCellIdentifier];
  6. [self.view addSubview:tableView];
  7. //实现代理和数据源协议中的方法
  8. #pragma mark - UITableViewDelegate
  9. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
  10. {
  11.     return kDefaultCellHeight;
  12. }
  13. #pragma mark - UITableViewDataSource
  14. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  15. {
  16.     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kReuseCellIdentifier];
  17.     return cell;
  18. }
  19. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  20. {
  21.     return self.dataArray.count;
  22. }
复制代码
创建UITableView实例对象

  1. UITableView *tableView = [[UITableView alloc] initWithFrame:frame style:UITableViewStyleGrouped];
复制代码
initWithFrame: style: 方法源码如下:
  1. - (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)theStyle
  2. {
  3.     if ((self=[super initWithFrame:frame])) {
  4.         _style = theStyle;
  5.         //_cachedCells 用于保存正在显示的Cell对象的引用
  6.         _cachedCells = [[NSMutableDictionary alloc] init];
  7.         //在计算完每个 section 包含的 section 头部,尾部视图的高度,和包含的每个 row 的整体高度后,
  8.         //使用 UITableViewSection 对象对这些高度值进行保存,并将该 UITableViewSection 对象的引用
  9.         //保存到 _sections中。在指定完 dataSource 后,至下一次数据源变化调用 reloadData 方法,
  10.         //由于数据源没有变化,section 相关的高度值是不会变化,只需计算一次,所以需要缓存起来。
  11.         _sections = [[NSMutableArray alloc] init];
  12.         //_reusableCells用于保存存在但未显示在界面上的可复用的Cell
  13.         _reusableCells = [[NSMutableSet alloc] init];
  14.         self.separatorColor = [UIColor colorWithRed:.88f green:.88f blue:.88f alpha:1];
  15.         self.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
  16.         self.showsHorizontalScrollIndicator = NO;
  17.         self.allowsSelection = YES;
  18.         self.allowsSelectionDuringEditing = NO;
  19.         self.sectionHeaderHeight = self.sectionFooterHeight = 22;
  20.         self.alwaysBounceVertical = YES;
  21.         if (_style == UITableViewStylePlain) {
  22.             self.backgroundColor = [UIColor whiteColor];
  23.         }
  24.         [self _setNeedsReload];
  25.     }
  26.     return self;
  27. }
复制代码
我将需要关注的地方做了详细的注释,这里我们需要关注_cachedCells, _sections, _reusableCells 这三个变量的作用。
  设置数据源

  1. tableView.dataSource = self;
复制代码
下面是 dataSrouce 的 setter 方法源码:
  1. - (void)setDataSource:(id
  2.   
  3.    
  4. )newSource
  5. {
  6.     _dataSource = newSource;
  7.     _dataSourceHas.numberOfSectionsInTableView = [_dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)];
  8.     _dataSourceHas.titleForHeaderInSection = [_dataSource respondsToSelector:@selector(tableView:titleForHeaderInSection:)];
  9.     _dataSourceHas.titleForFooterInSection = [_dataSource respondsToSelector:@selector(tableView:titleForFooterInSection:)];
  10.     _dataSourceHas.commitEditingStyle = [_dataSource respondsToSelector:@selector(tableView:commitEditingStyle:forRowAtIndexPath:)];
  11.     _dataSourceHas.canEditRowAtIndexPath = [_dataSource respondsToSelector:@selector(tableView:canEditRowAtIndexPath:)];
  12.     [self _setNeedsReload];
  13. }
  14.   
复制代码
_dataSourceHas 是用于记录该数据源实现了哪些协议方法的结构体,该结构体源码如下:
  1. struct {
  2.         unsigned numberOfSectionsInTableView : 1;
  3.         unsigned titleForHeaderInSection : 1;
  4.         unsigned titleForFooterInSection : 1;
  5.         unsigned commitEditingStyle : 1;
  6.         unsigned canEditRowAtIndexPath : 1;
  7.     } _dataSourceHas;
复制代码
  记录是否实现了某协议可以使用布尔值来表示,布尔变量占用的内存大小一般为一个字节,即8比特。但该结构体使用了 bitfields 用一个比特(0或1)来记录是否实现了某协议,大大缩小了占用的内存。
  在设置好了数据源后需要打一个标记,告诉NSRunLoop数据源已经设置好了,需要在下一次循环中使用数据源进行布局。下面看看 _setNeedReload 的源码:
  1. - (void)_setNeedsReload
  2. {
  3.     _needsReload = YES;
  4.     [self setNeedsLayout];
  5. }
复制代码
在调用了 setNeedsLayout 方法后,NSRunloop 会在下一次循环中自动调用 layoutSubViews 方法。
  
       
  • 视图的内容需要重绘时可以调用 setNeedsDisplay 方法,该方法会设置该视图的 displayIfNeeded 变量为 YES ,NSRunLoop 在下一次循环检中测到该值为 YES 则会自动调用 drawRect 进行重绘。
       
  • 视图的内容没有变化,但在父视图中位置变化了可以调用 setNeedsLayout,该方法会设置该视图的 layoutIfNeeded 变量为YES,NSRunLoop 在下一次循环检中测到该值为 YES 则会自动调用 layoutSubViews 进行重绘。
       
  •   更详细的内容可参考 When is layoutSubviews called?
      
  设置代理

  1. tableView.delegate = self;
复制代码
下面是 delegate 的 setter 方法源码:
  1. - (void)setDelegate:(id
  2.   
  3.    
  4. )newDelegate
  5. {
  6.     [super setDelegate:newDelegate];
  7.     _delegateHas.heightForRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)];
  8.     _delegateHas.heightForHeaderInSection = [newDelegate respondsToSelector:@selector(tableView:heightForHeaderInSection:)];
  9.     _delegateHas.heightForFooterInSection = [newDelegate respondsToSelector:@selector(tableView:heightForFooterInSection:)];
  10.     _delegateHas.viewForHeaderInSection = [newDelegate respondsToSelector:@selector(tableView:viewForHeaderInSection:)];
  11.     _delegateHas.viewForFooterInSection = [newDelegate respondsToSelector:@selector(tableView:viewForFooterInSection:)];
  12.     _delegateHas.willSelectRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:willSelectRowAtIndexPath:)];
  13.     _delegateHas.didSelectRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)];
  14.     _delegateHas.willDeselectRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:willDeselectRowAtIndexPath:)];
  15.     _delegateHas.didDeselectRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:didDeselectRowAtIndexPath:)];
  16.     _delegateHas.willBeginEditingRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:willBeginEditingRowAtIndexPath:)];
  17.     _delegateHas.didEndEditingRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:didEndEditingRowAtIndexPath:)];
  18.     _delegateHas.titleForDeleteConfirmationButtonForRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:titleForDeleteConfirmationButtonForRowAtIndexPath:)];
  19. }
  20.   
复制代码
与设置数据源一样,这里使用了类似的结构体来记录代理实现了哪些协议方法。
  UITableView绘制

  由于在设置数据源中调用了 setNeedsLayout 方法打上了需要布局的 flag,所以会在 1/60 秒(NSRunLoop的循环周期)后自动调用 layoutSubViews。layoutSubViews 的源码如下:
  1. - (void)layoutSubviews
  2. {
  3.     //对子视图进行布局,该方法会在第一次设置数据源调用 setNeedsLayout 方法后自动调用。
  4.     //并且 UITableView 是继承自 UIScrollview ,当滚动时也会触发该方法的调用
  5.     _backgroundView.frame = self.bounds;
  6.     //在进行布局前必须确保 section 已经缓存了所有高度相关的信息
  7.     [self _reloadDataIfNeeded];   
  8.     //对 UITableView 的 section 进行布局,包含 section 的头部,尾部,每一行 Cell
  9.     [self _layoutTableView];
  10.     //对 UITableView 的头视图,尾视图进行布局
  11.     [super layoutSubviews];
  12. }
复制代码
  需要注意的是由于 UITableView 是继承于 UIScrollView,所以在 UITableView 滚动时会自动调用该方法,详细内容可以参考 When is layoutSubviews called?
  下面依次来看三个主要方法的实现。
   _reloadDataIfNeeded 的源码如下:
  1. UITableView *tableView = [[UITableView alloc] initWithFrame:frame style:UITableViewStyleGrouped];0
复制代码
其中 _updateSectionsCashe 方法是最重要的,该方法在数据源更新后至下一次数据源更新期间只能调用一次,该方法的源码如下:
  1. UITableView *tableView = [[UITableView alloc] initWithFrame:frame style:UITableViewStyleGrouped];1
复制代码
我在需要注意的地方加了注释,上面方法主要是记录每个 Cell 的高度和整个 section 的高度,并把结果同过 UITableViewSection 对象缓存起来。
  _layoutTableView 的源码实现如下:

  1. UITableView *tableView = [[UITableView alloc] initWithFrame:frame style:UITableViewStyleGrouped];2
复制代码
  关于 UIView 的 frame 和bounds 的区别可以参考 What's the difference between the frame and the bounds?
  这里使用了三个容器 _cachedCells, availableCells, _reusableCells 完成了 Cell 的复用,这是 UITableView 最核心的地方。
  下面一起看看三个容器在创建到滚动整个过程中所包含的元素的变化情况。
  在第一次设置了数据源调用该方法时,三个容器的内容都为空,在调用完该方法后 _cachedCells 包含了当前所有可视 Cell 与其对应的indexPath 的键值对,availableCells 与 _reusableCells 仍然为空。只有在滚动起来后 _reusableCells 中才会出现多余的未显示可复用的 Cell。
  刚创建 UITableView 时的状态如下图(红色为屏幕内容即可视区域,蓝色为超出屏幕的内容,即不可视区域):
     

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

UITableView的Cell复用原理和源码分析-1-技术控-控制器,开发者,数据源,Excel,文章
   初始状态.png
    如图,当前 _cachedCells 的元素为当前可视的所有 Cell 与其对应的 indexPath 的键值对。
  向上滚动一个 Cell 的过程中,由于 availableCells 为 _cachedCells 的拷贝,所以可根据 indexPath 直接取到对应的 Cell,这时从底部滚上来的第7行,由于之前的 _reusableCells 为空,所以该 Cell 是直接创建的而并非复用的,由于顶部 Cell 滚动出了可视区域,所以被加入了 _reusableCells 中以便后续滚动复用。滚动完一行后的状态变为了 _cachedCells 包含第 2 行到第 7 行 Cell 的引用,_reusableCells 包含第一行 之前滚动出可视区域的第一行 Cell 的引用。
     

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

UITableView的Cell复用原理和源码分析-2-技术控-控制器,开发者,数据源,Excel,文章
   向上滚动1个Cell.png
    当向上滚动两个 Cell 的过程中,同理第 3 行到第 7 行的 Cell 可以通过对应的 indexPath 从 _cachedCells 中获取。这时 _reusableCells 中正好有一个可以复用的 Cell 用来从底部滚动上来的第 8 行。滚动出顶部的第 2 行 Cell 被加入 _reusableCells 中。
  

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

UITableView的Cell复用原理和源码分析-3-技术控-控制器,开发者,数据源,Excel,文章

  总结

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



上一篇:Three roads lead to Rome
下一篇:Goto对变量定义的影响
西藏生活网 发表于 2016-12-23 09:50:43
你女儿在我手上,我不是你女婿。
回复 支持 反对

使用道具 举报

*滑动验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

我要投稿

推荐阅读


回页顶回复上一篇下一篇回列表
手机版/CoLaBug.com ( 粤ICP备05003221号 | 文网文[2010]257号 )

© 2001-2017 Comsenz Inc. Design: Dean. DiscuzFans.

返回顶部 返回列表