来源: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实例对象
UITableView *tableView = [[UITableView alloc] initWithFrame:frame style:UITableViewStyleGrouped];
initWithFrame: style: 方法源码如下:
我将需要关注的地方做了详细的注释,这里我们需要关注_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 的源码如下:
其中 _updateSectionsCashe 方法是最重要的,该方法在数据源更新后至下一次数据源更新期间只能调用一次,该方法的源码如下:
我在需要注意的地方加了注释,上面方法主要是记录每个 Cell 的高度和整个 section 的高度,并把结果同过 UITableViewSection 对象缓存起来。
_layoutTableView 的源码实现如下:
关于 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 时的状态如下图(红色为屏幕内容即可视区域,蓝色为超出屏幕的内容,即不可视区域):
如图,当前 _cachedCells 的元素为当前可视的所有 Cell 与其对应的 indexPath 的键值对。
向上滚动一个 Cell 的过程中,由于 availableCells 为 _cachedCells 的拷贝,所以可根据 indexPath 直接取到对应的 Cell,这时从底部滚上来的第7行,由于之前的 _reusableCells 为空,所以该 Cell 是直接创建的而并非复用的,由于顶部 Cell 滚动出了可视区域,所以被加入了 _reusableCells 中以便后续滚动复用。滚动完一行后的状态变为了 _cachedCells 包含第 2 行到第 7 行 Cell 的引用,_reusableCells 包含第一行 之前滚动出可视区域的第一行 Cell 的引用。
当向上滚动两个 Cell 的过程中,同理第 3 行到第 7 行的 Cell 可以通过对应的 indexPath 从 _cachedCells 中获取。这时 _reusableCells 中正好有一个可以复用的 Cell 用来从底部滚动上来的第 8 行。滚动出顶部的第 2 行 Cell 被加入 _reusableCells 中。
总结
到此你已经了解了 UITableView 的 Cell 的复用原理,可以根据需要定制出更复杂的控件。
程序员共读↓↓↓
Java编程精选↓↓↓
更多推荐《年薪百万的程序员都在干什么?》
涵盖:程序人生、算法与数据结构、黑客技术与网络安全、大数据技术、前端开发、Java、Python、Web开发、安卓开发、iOS开发、C/C++、.NET、Linux、数据库、运维等
文章浏览阅读2.5k次,点赞5次,收藏17次。2018年3月,我与张老师就这么在微信上聊了起来,起初我并没有写书的打算,我们之间只是通过讨论、交流的形式聊聊关于出书的方方面面。最终,敌不过张老师超强的专业能力、细致的解说与盛情相邀,我答应张老师写一本Linux系统运维的图书并由人邮出版。由此,我踏上了漫漫2年多的写书之路。为什么写这本书写书一方面是我对自己所学知识的查漏补缺过程,另一方面也可以向即将进入或已经入行的Linux系统运维同..._linux系统运维指南:从入门到企业实战 pdf
文章浏览阅读2k次,点赞6次,收藏5次。tf.reduce_sum()函数深度解析从矩阵,数组,数据存储的角度 解析axis参数的意义_tf.reduce_sum
文章浏览阅读9.8k次,点赞4次,收藏29次。adb获取app包名的方法_adb获取包名
文章浏览阅读913次,点赞16次,收藏10次。总之,要做好虾皮店铺,不仅需要明确的定位和优质的产品,还需要精心的运营和持续的改进。通过不断优化店铺形象、制定有效的营销策略、提供优质的客户服务以及加强供应链管理等手段,您将能够在激烈的竞争中脱颖而出,实现店铺的长足发展。1.稳定的网络环境是基石,它需要经过技术手段的洗礼,将电脑或手机的底层硬件参数伪装成国外数据,以躲避平台通过IP进行的深度检测。这种真实性高的评价能够帮助商家获得更多的信任和认可,从而提升产品的排名和流量的分配。您可以关注行业动态,学习先进的经营理念和技术,以提高店铺的运营水平。
文章浏览阅读5k次,点赞11次,收藏43次。统计检验_统计测试 cd diagrams
文章浏览阅读332次。购物车_购物车案例请求数据地址
文章浏览阅读2.9k次,点赞3次,收藏37次。1. IIS1. PUT漏洞用户配置不当,exp:https://github.com/hackping/HTTPMLScan.git2. 短文件名猜解IIS的短文件名机制,可以暴力猜解短文件名,访问构造的某个存在的短文件名,会返回404,访问构造的某个不存在的短文件名,返回400。exp:https://github.com/WebBreacher/tilde_enum3.远程代码执行(CVE-2017-7269))**exp**:https://github.com/zcgonv_hrs中间件
文章浏览阅读368次。DB2支持以下两种类型的表空间: 1、 系统管理存储器表空间(SMS-SYSTEM MANAGED STORAGE) 2、 数据库管理存储器表空间(DMS-DATABASE MANAGED STORAGE) SMS、DMS用户表空间的特性对照 特性 ..._db2
文章浏览阅读84次。正在开发中的游戏有个全屏功能--可以在window桌面背景上运行,就像一些视频播放器在桌面背景上播放一样的,花了个上午整了个Demo放出来留个纪念。实现功能:显示图标,双击图标执行相应的程序,右击图标弹出该图标对应得菜单,点击非图标区则弹出桌面菜单。需要完整工程可以点此下载:DesktopWindow.rar。程序效果图如下:在这个程序里,定义了一个XShellItem..._模拟实现windows桌面效果
文章浏览阅读944次。https://www.byhy.net/tut/webdev/django/01/_byhy.net
文章浏览阅读5.8k次,点赞13次,收藏57次。业务场景介绍:H5移动端支持微信支付 [ 微信支付分为微信内支付(JSAPI支付官方API)和微信外支付(H5支付官方API)] && 支付宝支付 [手机网站支付转 APP 支付 官方API ]订单生成逻辑:前端请求后端提交订单,后端去和微信或者支付宝对接生成订单(后续支付都是这个逻辑进行的对接)一、移动端微信支付,vue中如何玩?在移动端微信支付分为微信内支付和微信外支付。1.在订单组件中选择支付方式之后在支付页面先去判断是否是在微信内://判断是否微信 is__移动端支付宝微信支付vue项目怎么写
文章浏览阅读2k次,点赞5次,收藏9次。深度学习编译器主要为解决不同框架下训练的模型部署到指定的某些设备上时所遇到的一系列复杂的问题,即将各种深度学习训练框架的模型部署到各种硬件所面临的问题;_tvm编译器