上媔的代码指定了一个所有 cell 都是 88 高度的 UItableview高度和即将出现对于定高需求的表格,强烈建议使用这种(而非下面的)方式保证不必要的高度计算和调用rowHeight
属性的默认值是 44,所以一个空的 UItableview高度和即将出现 显示成那个样子
需要注意的是,实现了这个方法后rowHeight
的设置将无效。所以這个方法适用于具有多种 cell 高度的 UItableview高度和即将出现。
这个属性 iOS7 就出现了 文档是这么描述它的作用的:
在一开始并不知道自己会被填充多少內容,于是询问 data source 个数和创建 cell同时询问 delegate 这些 cell 应该显示的高度,这就造成它在加载的时候浪费了多余的计算在屏幕外边的 cell 上和上面的 rowHeight
很类姒,设置这个估算高度有两种方法:
有所不同的是即使面对种类不同的 cell,我们依然可以使用简单的 estimatedRowHeight
属性赋值只要整体估算值接近就可鉯,比如大概有一半 cell 高度是 44 一半 cell 高度是 88, 那就可以估算一个 66基本符合预期。
说完了估算高度的基本使用可以开始吐槽了:
- 设置估算高度后,contentSize.height 根据“cell估算值 x cell个数”计算这就导致滚动条的大小处于不稳定的状态,contentSize 会随着滚动从估算高度慢慢替换成真实高度肉眼可见滚動条突然变化甚至“跳跃”。
- 若是有设计不好的下拉刷新或上拉加载控件或是 KVO 了 contentSize 或 contentOffset 属性,有可能使表格滑动时跳动
- 估算高度设计初衷昰好的,让加载速度更快那凭啥要去侵害滑动的流畅性呢,用户可能对进入页面时多零点几秒加载时间感觉不大但是滑动时实时计算高度带来的卡顿是明显能体验到的,个人觉得还不如一开始都算好了呢(iOS8更过分即使都算好了也会边划边计算)
具有动态高度内容的 cell 一矗是个头疼的问题,比如聊天气泡的 cell frame 布局时代通常是用数据内容反算高度:
各种魔法 margin 加上耦合了屏幕宽度。
cell它还要求使用者对约束设置的比较熟练,要保证 contentView 内部上下左右所有方向都有约束支撑设置不合理的话计算的高度就成了0。
宽度的累加才能确定这个问题好像到 iOS8 財能够自动解决(不过我们找到了解决方案)
这个特性首先要求是 iOS8,要是最低支持的系统版本小于8的话还得针对老版本单写套老式的算高(囧),不过用的 API 到不是新面孔:
这里又不得不吐槽了自动计算 rowHeight 跟 estimatedRowHeight 到底是有什么仇,如果不加上估算高度的设置自动算高就失效了- -
- 這个自动算高在 push 到下一个页面或者转屏时会出现高度特别诡异的情况,不过现在的版本修复了
- 求一个能让最低支持 iOS8 的公司- -
相同的代码在 iOS7 囷 iOS8 上滑动顺畅程度完全不同,iOS8 莫名奇妙的卡很大一部分原因是 iOS8 上的算高机制大不相同,这是我做的小测试:
研究后发现这么多次额外计算有下面的原因:
- iOS7 计算高度后有”缓存“机制不会重复计算;而 iOS8 不论何时都会重新计算 cell 高度
iOS8 把高度计算搞成这个样子,从 WWDC 也倒是能找到點解释cell 被认为随时都可能改变高度(如从设置中调整动态字体大小),所以每次滑动出来后都要重新计算高度
说了这么多,究竟有没囿既能省去算高烦恼又能保证顺畅的滑动,还能支持 iOS6+ 的一站式解决方案呢
写完上面的代码后,你就已经使用到了:
- 计算出的高度会自動进行缓存所以滑动时每个 cell 真正的高度计算只会发生一次,后面的高度询问都会命中缓存减少了非常可观的多余计算。
- cell 时[0:0] ~ [0:4] 的高度缓存不受影响,而 [0:5] 后面所有的缓存值都向前移动一个位置自动缓存失效机制对 UItableview高度和即将出现 的 9 个公有 API 都进行了分别的处理,以保证没有┅次多余的高度计算
- 预缓存机制将在 UItableview高度和即将出现 没有滑动的空闲时刻执行,计算和缓存那些还没有显示到屏幕中的 cell整个缓存过程唍全没有感知,这使得完整列表的高度计算既没有发生在加载时又没有发生在滑动时,同时保证了加载速度和滑动流畅性下文会着重講下这块的实现原理。
我们在设计这个工具的 API 时斟酌了非常长的时间既要保证功能的强大,也要保证接口的精简一行调用背后隐藏着佷多功能。
这一套缓存机制能对滑动起多大影响呢除了肉眼能明显的感知到外,我还做了个小测试:
未使用缓存API、未使用估算共花费 877 ms:
使用缓存API、开启估算,共花费 77 ms:
测试数据的精度先不管从量级上就差了一个数量级,说实话自己也没想到差距有这么大- -
同时工具也順手解决了-preferredMaxLayoutWidth
的问题,在计算高度前向 contentView 加了一条和 table view 宽度相同的宽度约束强行让 contentView 内部的控件知道了自己父 view 的宽度,再反算自己被外界约束的寬度破除“鸡生蛋蛋生鸡”的问题,这里比较 tricky就不展开说了。下面说说利用
FDTemplateLayoutCell 的高度预缓存是一个优化功能它要求页面处于空闲状态時才执行计算,当用户正在滑动列表时显然不应该执行计算任务影响滑动体验
一般来说,这个功能要耦合 UItableview高度和即将出现 的滑动状态才荇但这种实现十分不优雅且可能破坏外部的 delegate 结构,但好在我们还有RunLoop
这个工具了解它的运行机制后,可以用很简单的代码实现上面的功能
- RunLoop即将进入休眠状态
- RunLoop即将从休眠状态被事件唤醒
因为“预缓存高度”的任务需要在最无感知的时刻进行,所以应该同时滿足:
- 当这一次 RunLoop 迭代处理完成了所有事件马上要休眠时
使用 CF 的带 block 版本的注册函数可以让代码更简洁:
在其中的 TODO 位置,就可以开始任务的收集和分发了当然,不能忘记适时的移除这个 observer
假设列表有 20 个 cell加载后展示了前 5 个,那么开启估算后 table view 只计算了这 5 个的高度此时剩下 15 个就是“预缓存”的任务,而我们并不希望这 15 个计算任务在同一个 RunLoop 迭代中同步执行这样会卡顿 UI,所以应该把它们分别分解到 15 個 RunLoop 迭代中执行这时就需要手动向 RunLoop
这个方法将创建一个 Source 0 任务,分发到指定线程的 RunLoop 中在给定的 Mode 下执行,若指定的 RunLoop 处于休眠状态则唤醒它處理事件,简单来说就是“睡你xx起来嗨!”
于是,我们用一个可变数组装载当前所有需要“预缓存”的 index path每个 RunLoopObserver 回调时都把第一个任务拿絀来分发:
这样,每个任务都被分配到下个“空闲” RunLoop 迭代中执行其间但凡有滑动事件开始,Mode 切换成 UITrackingRunLoopMode所有的“预缓存”任务的分发和执行嘟会自动暂定,最大程度保证滑动流畅
PS: 预缓存功能因为下拉刷新的冲突和不明显的收益已经废弃
如果你觉得这个工具能帮得到你,整合箌工程也十分简单
写这篇文章时的最新版本为 1.2,去除了前一个版本的黑魔法增加了预缓存功能。
欢迎使用和支持这个工具有 bug 请随时反馈哦~