UITableView-FDTemplateLayoutCell 源码分析
以下简称 UITableView-FDTemplateLayoutCell
为 FDT
。
FDT
FDT 的作用在我看来就是可以缓存 Cell 高度。如果你还不知道为什么需要缓存 Cell 高度的话,可以先看下这篇文章 UITableViewCell 自动高度,了解下 iOS8 开始如何实现 Cell 的自动算高,并且为什么需要缓存已经计算过的 Cell 的高度。
那么只是实现缓存高度的话,为什么还需要分析 FDT 的源码呢?因为 FDT 源码写得很好,要不那么多 star 呢、群众的眼睛是雪亮的。就这一点就足以让我们分析下 FDT 是如何实现代码的。
明确目标
通过 UITableViewCell 自动高度,我们知道了要实现高度缓存我们需要做的工作如下:
- 决定合适的 Cache Key
- 选取合适的 Cache Storage
- 在 Delete/Insert 发生时调整缓存数据
于是我们就可以以这几个为目标分析下它们在 FDT 中的实现。
Cache Key
在 UITableViewCell 自动高度 中我们已经知道了,indexPath
是可以作为 Cache Key 的,那是我们但从 Cell 角度考虑后的结论。但是 FDT 是很用心的,它除了提供了 indexPath
作为 Cache Key 的方式,还提供了另外一个 API:
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier
cacheByKey:(id<NSCopying>)key
configuration:(void (^)(id cell))configuration;
这个 API 可以让我自由的决定 Cache Key 的来源,并且 FDT 还很贴心的告诉我们,可以使用 unique entity identifier
。
我们知道数据库中的内容可以通过 ORM 变成 relational entities,比如你有一个 Student 表,里面每一行都是一个学生的信息,于是 ORM 之后每行就是一个 entity,而 primary key(通常就是 autoincrement-id) 就可以作为 Cache Key。
这是 FDT 的相关例子:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
Entity *entity = self.entities[indexPath.row];
return [tableView fd_heightForCellWithIdentifier:@"identifer" cacheByKey:entity.uid configuration:^(id cell) {
// configurations
}];
}
Cache Storage
在 UITableViewCell 自动高度 中我们已经分析了一个可以作为 Storage 的候选,最后得出 objc_setAssociatedObject
和 NSMutableArray
性能要大幅度优于 NSCache
。那么我们可以看看 FDT 的实现是不是符合我们的期望。
通过阅读源码,我们会发现在 FDT 中同时使用了 objc_setAssociatedObject
和 NSMutableArray
。
objc_setAssociatedObject
先说说使用 objc_setAssociatedObject
的地方,分别是:
我们就看两个典型的,它们有下面的特性:
- lazy initialized
-
OBJC_ASSOCIATION_RETAIN_NONATOMIC
到了 UITableView 实例上
这么做的好处呢?很明显啊:
- lazy initialized,就是在用到相关的缓存策略时才会初始化其 Cache Storage
- 将 Cache 的释放托管给了 UITableView 实例的生命周期,不用管释放内存的事情了
也是由于第 2 点优势(或者说需求)吧,那些地方才用了 objc_setAssociatedObject
。所以说具体用什么是酌情而定的,没有银弹。
NSMutableArray
那么哪里用了 NSMutableArray
来作为 Cache Storage 呢?分别是:
还是就看两个典型。我们注意到,不论是 KeyedHeightCache
和 IndexPathHeightCache
都将缓存数据放到了 NSMutableArray
。为什么这里就不用 objc_setAssociatedObject
了呢?
回忆我们在 UITableViewCell 自动高度 中分析的,如果使用了 indexPath
作为 Cache Key,那么在发生了 Delete/Inert
之后,缓存中的 Cache Key
就『不准』了,需要进行相应的变动,那么采用了 NSMutableArray
它能为我们自动的变动。
另外在采用 IndexPathHeightCache
时,FDT 的缓存是一个二维的数组,头一维定位到 Section,后一维定位到 Row,这样就可以同时管到 Sections
和 Rows
的数据变动。为什么这里要提一下呢,因为一般说到使用 indexPath
做 Cache Key,那么首先想到的就是:
cacheKey = fmt("%d-%d", indexPath.section, indexPath.row)
但是这样的 Key 生成方式不能适应我们这里需要同时灵活处理 Sections
和 Rows
变动的情况。
在 Delete/Insert 发生时调整缓存数据
这一步其实是 FDT 为我们做得最重要的一步,因为自己实现缓存存储还是相对简单的,但是在一有 Section/Rows 发生 Delete/Insert
就及时更改缓存数据是有点麻烦的。
如果让我去实现在 Section/Row 发生 Delete/Insert
就及时更改缓存数据,我会这么做(请叫我反面教材😆):
- 继承
UITableView
产生MCUITableView
- 在
MCUITableView
中重写涉及 Section/Row 的Delete/Insert
的方法以调整缓存数据 - 同时
MCUITableView
会向外暴露一个属性mcDelegate
,这个属性是UITableViewDelegate
类型,使用者需要设置这个属性。在我的重写方法中发现使用者实现了UITableViewDelegate
中的同名方法时会调用它们,这样使得使用起来和UITableView
一样
既然是反面教材,那么这个方案的缺点是:
- 实现起来不灵活,多处重写
- 使用起来不灵活,需要更改很多已有的代码,原来继承 UITableView 的现在需要继承
MCUITableView
FDT 使用 Category 的方式提供了便捷的 API,肯定不能在这里掉链子。重写肯定是绕不开的,FDT 采用了更加灵活的方式来完成重写 method_exchangeImplementations
,代码在 L156,通过将 UITableView 的相关方法使用 method_exchangeImplementations
和 FDT 自己的方法调换一下,真是很巧妙的。
IndexPathHeightCache or KeyedHeightCache
IndexPathHeightCache
和 KeyedHeightCache
在实现和使用上还有些区别:
-
IndexPathHeightCache
在实现上需要在 Section/Row 发送变动时调整缓存,而KeyedHeightCache
不需要 -
IndexPathHeightCache
使用了indexPath
作为 Key,而KeyedHeightCache
需要你自己确保 Key 是唯一的 - 如果你自己可以确保 Key 是唯一的,那么
KeyedHeightCache
肯定是会稍微快点的(因为不需要调整缓存)
estimatedRowHeight
estimatedRowHeight 的好处我们在 UITableViewCell 自动高度 中已经说过了。
但是,你用了 FDT 之后,就不要设置这个值了,FDT 提倡的就是让 UITableView 一次计算所有的 Cell's height,原文见 cell-height-calculation
估算高度设计初衷是好的,让加载速度更快,那凭啥要去侵害滑动的流畅性呢,用户可能对进入页面时多零点几秒加载时间感觉不大,但是滑动时实时计算高度带来的卡顿是明显能体验到的,个人觉得还不如一开始都算好了呢(iOS8更过分,即使都算好了也会边划边计算)
如果你非要用的话,反正我在 FDT 提供了 Demo 中设置了结果是这样:
如果非要用的话三思吧😄
疑惑
目前发现的 FDT 源码中这一处关于 methodSignatureForSelector
让我很困惑调用的作用是什么,见 L122,我注释掉也没发现什么问题 😂,一定是我打开的方式不对,总之希望有明白可以告诉我吧,不甚感激😝。
最后,happy coding!