UITableView+FDTemplateLayoutCell是一个优化计算cell高度以追求性能的轻量级框架,虽然Apple在这方面也不断作出改变以求达到优化效果,但彷佛成效并不那么顺利,详情能够阅读该框架制做团队的博文 优化UITableViewCell高度计算的那些事。html
经过本文你能够阅读到:git
首先,咱们先分析框架的组成,github地址:传送门github
能够看到,框架只提供了4个类,能够说是十分轻量级的。但为了尽可能简化的去学习,咱们先除去用来打印debug信息的UITableView+FDTemplateLayoutCellDebug
。同时,由于UITableView+FDKeyedHeightCache
和UITableView+FDIndexPathHeightCache
实际上是两套cell高度缓存机制,那么咱们能够二选一先进行学习,瞄了一眼二者的代码量,你应该也是果断选择了前者吧?😆swift
通过一番筛选,咱们的探讨重点缩小为:缓存
接下来,咱们主要以框架的demo开始进行学习。bash
如日常咱们使用UITableView同样,设置完reuseIdentifier
和初始数据后,咱们进行UITableView的Data Source
和Delegate
配置。数据结构
能够发现,该框架对Data Source
部分无代码侵入性,但对Delegate
中- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
部分存在代码侵入性。app
咱们主要观察FDSimulatedCacheModeCacheByKey
这个case:框架
FDFeedEntity *entity = self.feedEntitySections[indexPath.section][indexPath.row];
return [tableView fd_heightForCellWithIdentifier:@"FDFeedCell"
cacheByKey:entity.identifier
configuration:^(FDFeedCell *cell) {
// 主要用来设置cell的样式`accessoryType`和数据`entity`,即对cell进行配置。
[self configureCell:cell atIndexPath:indexPath];
}];
复制代码
咱们对一个框架的评价也包括其对项目源码的入侵性,无入侵性则优。而该框架成功的在Data Source
部分作到无入侵性,但为什么不得不在返回cell高度这个Delegate中作这种具入侵性的行为?咱们点进去看看。ide
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id<NSCopying>)key configuration:(void (^)(id cell))configuration {
// 1
if (!identifier || !key) {
return 0;
}
// 2
// Hit cache
if ([self.fd_keyedHeightCache existsHeightForKey:key]) {
CGFloat cachedHeight = [self.fd_keyedHeightCache heightForKey:key];
[self fd_debugLog:[NSString stringWithFormat:@"hit cache by key[%@] - %@", key, @(cachedHeight)]];
return cachedHeight;
}
// 3
CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
[self.fd_keyedHeightCache cacheHeight:height byKey:key];
[self fd_debugLog:[NSString stringWithFormat:@"cached by key[%@] - %@", key, @(height)]];
// 4
return height;
}
复制代码
一步步来探讨:
cell无重用标识符或者缓存key值为空,则height值返回0;
这比较容易理解,reuseIdentifier
为空去cell重用池固然取不回对应的cell。用值为空的key去fd_keyedHeightCache
缓存池固然也取不回对应的高度值。fd_keyedHeightCache
在步骤2介绍。
命中缓存,根据key值从key-height缓存池中取出对应的height值。
fd_keyedHeightCache
:设置该关联属性的目的是建立key-height缓存池,其类型为FDKeyedHeightCache
,底层经过NSMutableDictionary<id<NSCopying>, NSNumber *>
做为key-height关系进行一一对应的存储,并提供多种方法,后面再细说。
没有命中缓存,先计算出height值,再将key-height对应关系放入在key-height缓存池。
返回计算完成并被缓存好的height值。
从上面的步骤中咱们初步知道入侵性代码大体都作了什么,但并无过多的深刻了解,主要包括:一是FDKeyedHeightCache
的数据结构,二是cell高度的计算实现。
这两点偏偏是该框架的核心内容。
FDKeyedHeightCache部分的代码量很是少且容易理解,这里主要提一下缓存失效问题。
FDKeyedHeightCache提供了两种途径,分别是使指定key的height失效方法:- (void)invalidateHeightForKey:(id<NSCopying>)key;
和使整个key-height缓存池失效方法:- (void)invalidateAllHeightCache;
。
那么断定key-height失效的依据是什么?
咱们能够从下面这段代码中看出其tricky:
- (BOOL)existsHeightForKey:(id<NSCopying>)key {
NSNumber *number = self.mutableHeightsByKeyForCurrentOrientation[key];
return number && ![number isEqualToNumber:@-1];
}
复制代码
咱们能够看到,断定失效的本质依据是:height值为-1时,key-height失效,该断定一样适用于FDIndexPathHeightCache缓存机制。
自动的缓存失效机制(本质处理是将height值设为-1,或者清空高度缓存池)
无须担忧你数据源的变化引发的缓存失效,当调用如-reloadData,-deleteRowsAtIndexPaths:withRowAnimation:等任何一个触发 UITableView 刷新机制的方法时,已有的高度缓存将以最小的代价执行失效。如删除一个 indexPath 为 [0:5] 的 cell 时,[0:0] ~ [0:4] 的高度缓存不受影响,而 [0:5] 后面全部的缓存值都向前移动一个位置。自动缓存失效机制对 UITableView 的 9 个公有 API 都进行了分别的处理,以保证没有一次多余的高度计算。
cell高度计算能够说是该框架中最复杂的部分,咱们须要先对template layout cell的理解有个大体概念:能够把template layout cell当作是一个占位的cell。
咱们继续点进去相关的代码:
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration {
// 1
if (!identifier) {
return 0;
}
// 2
UITableViewCell *templateLayoutCell = [self fd_templateCellForReuseIdentifier:identifier];
// 3
// Manually calls to ensure consistent behavior with actual cells. (that are displayed on screen)
[templateLayoutCell prepareForReuse];
// 4
// Customize and provide content for our template cell.
if (configuration) {
configuration(templateLayoutCell);
}
// 5
return [self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell];
}
复制代码
一步步来探讨:
无重用标识符则height值返回0;
根据重用标识符获取templateLayoutCell;
cell在从dequeueReusableCellWithIdentifier:
取出以后,若是须要作一些额外的计算,好比说计算cell高度,手动调用prepareForReuse
以确保与实际cell(显示屏幕上)的行为一致;
主要是在外部调用的block里为templateLayoutCell提供数据,以及对其进行一些自定义;
经过templateLayoutCell真正计算height值。
咱们再对步骤2和5进行深刻的解析,而这两点偏偏是高度计算的核心:
点进去方法实现:
- (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier {
// 1
NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
// 2
NSMutableDictionary<NSString *, UITableViewCell *> *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
// 3
if (!templateCellsByIdentifiers) {
templateCellsByIdentifiers = @{}.mutableCopy;
objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
// 4
UITableViewCell *templateCell = templateCellsByIdentifiers[identifier];
// 5
if (!templateCell) {
templateCell = [self dequeueReusableCellWithIdentifier:identifier];
NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier);
templateCell.fd_isTemplateLayoutCell = YES;
templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO;
templateCellsByIdentifiers[identifier] = templateCell;
[self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]];
}
// 6
return templateCell;
}
复制代码
继续一步步探讨:
对identifier
断言,这好理解;
获取identifier-templateCell缓存池templateCellsByIdentifiers
;
templateCellsByIdentifiers
的类型为NSMutableDictionary<NSString *, UITableViewCell *>
若是缓存池templateCellsByIdentifiers
不存在,则建立一个,并设置成关联属性;
根据标识符identifier
在identifier-templateCell缓存池中取出templateCell,找不到则返回nil;
在templateCell缓存池找不到对应的templateCell的话,会先去系统的cell复用池中查找,若是没有注册对应的identifier
,会被断言,找到后则赋值给templateCell,被标记为fd_isTemplateLayoutCell
,且其内容布局会变成frame layout
,最后该templateCell会被放入identifier-templateCell缓存池中。
被标记为fd_isTemplateLayoutCell
的缘由源码中也有解释:
/// Indicate this is a template layout cell for calculation only.
/// You may need this when there are non-UI side effects when configure a cell.
/// Like:
/// - (void)configureCell:(FooCell *)cell atIndexPath:(NSIndexPath *)indexPath {
/// cell.entity = [self entityAtIndexPath:indexPath];
/// if (!cell.fd_isTemplateLayoutCell) {
/// [self notifySomething]; // non-UI side effects
/// }
/// }
///
复制代码
经过判断cell是否为templateCell,若是是则表示在配置cell时只进行布局计算,不去作UI相关的改动。
跳进其实现方法,长达100多行的代码着实显示出其份量,但过程并不复杂,咱们来看看:
- (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell {
// 1. 拿到tableView的宽度
CGFloat contentViewWidth = CGRectGetWidth(self.frame);
// 2. 将cell的宽度设置成跟tableView同样宽
CGRect cellBounds = cell.bounds;
cellBounds.size.width = contentViewWidth;
cell.bounds = cellBounds;
// 3. 拿到快速索引的宽度(若是有)
CGFloat rightSystemViewsWidth = 0.0;
for (UIView *view in self.subviews) {
if ([view isKindOfClass:NSClassFromString(@"UITableViewIndex")]) {
rightSystemViewsWidth = CGRectGetWidth(view.frame);
break;
}
}
// 4. 主要是计算Accessory view的宽度。
// If a cell has accessory view or system accessory type, its content view's width is smaller // than cell's by some fixed values.
if (cell.accessoryView) {
rightSystemViewsWidth += 16 + CGRectGetWidth(cell.accessoryView.frame);
} else {
static const CGFloat systemAccessoryWidths[] = {
[UITableViewCellAccessoryNone] = 0,
[UITableViewCellAccessoryDisclosureIndicator] = 34,
[UITableViewCellAccessoryDetailDisclosureButton] = 68,
[UITableViewCellAccessoryCheckmark] = 40,
[UITableViewCellAccessoryDetailButton] = 48
};
rightSystemViewsWidth += systemAccessoryWidths[cell.accessoryType];
}
// 5. 应该是判断设备是不是i6plus
if ([UIScreen mainScreen].scale >= 3 && [UIScreen mainScreen].bounds.size.width >= 414) {
rightSystemViewsWidth += 4;
}
// 6. cell实际contentView宽度大小
contentViewWidth -= rightSystemViewsWidth;
// 7. 下面已经给出了接下来计算流程的注释,这里就再也不过多解释
// If not using auto layout, you have to override "-sizeThatFits:" to provide a fitting size by yourself.
// This is the same height calculation passes used in iOS8 self-sizing cell's implementation. // // 1. Try "- systemLayoutSizeFittingSize:" first. (skip this step if 'fd_enforceFrameLayout' set to YES.) // 2. Warning once if step 1 still returns 0 when using AutoLayout // 3. Try "- sizeThatFits:" if step 1 returns 0 // 4. Use a valid height or default row height (44) if not exist one CGFloat fittingHeight = 0; if (!cell.fd_enforceFrameLayout && contentViewWidth > 0) { // Add a hard width constraint to make dynamic content views (like labels) expand vertically instead // of growing horizontally, in a flow-layout manner. NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth]; // [bug fix] after iOS 10.3, Auto Layout engine will add an additional 0 width constraint onto cell's content view, to avoid that, we add constraints to content view's left, right, top and bottom. static BOOL isSystemVersionEqualOrGreaterThen10_2 = NO; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ isSystemVersionEqualOrGreaterThen10_2 = [UIDevice.currentDevice.systemVersion compare:@"10.2" options:NSNumericSearch] != NSOrderedAscending; }); NSArray<NSLayoutConstraint *> *edgeConstraints; if (isSystemVersionEqualOrGreaterThen10_2) { // To avoid confilicts, make width constraint softer than required (1000) widthFenceConstraint.priority = UILayoutPriorityRequired - 1; // Build edge constraints NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0]; NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeRight multiplier:1.0 constant:-rightSystemViewsWidth]; NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeTop multiplier:1.0 constant:0]; NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:cell attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0]; edgeConstraints = @[leftConstraint, rightConstraint, topConstraint, bottomConstraint]; [cell addConstraints:edgeConstraints]; } [cell.contentView addConstraint:widthFenceConstraint]; // Auto layout engine does its math fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; // Clean-ups [cell.contentView removeConstraint:widthFenceConstraint]; if (isSystemVersionEqualOrGreaterThen10_2) { [cell removeConstraints:edgeConstraints]; } [self fd_debugLog:[NSString stringWithFormat:@"calculate using system fitting size (AutoLayout) - %@", @(fittingHeight)]]; } if (fittingHeight == 0) { #if DEBUG // Warn if using AutoLayout but get zero height. if (cell.contentView.constraints.count > 0) { if (!objc_getAssociatedObject(self, _cmd)) { NSLog(@"[FDTemplateLayoutCell] Warning once only: Cannot get a proper cell height (now 0) from '- systemFittingSize:'(AutoLayout). You should check how constraints are built in cell, making it into 'self-sizing' cell."); objc_setAssociatedObject(self, _cmd, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } #endif // Try '- sizeThatFits:' for frame layout. // Note: fitting height should not include separator view. fittingHeight = [cell sizeThatFits:CGSizeMake(contentViewWidth, 0)].height; [self fd_debugLog:[NSString stringWithFormat:@"calculate using sizeThatFits - %@", @(fittingHeight)]]; } // Still zero height after all above. if (fittingHeight == 0) { // Use default row height. fittingHeight = 44; } // Add 1px extra space for separator line if needed, simulating default UITableViewCell. if (self.separatorStyle != UITableViewCellSeparatorStyleNone) { fittingHeight += 1.0 / [UIScreen mainScreen].scale; } return fittingHeight; } 复制代码
关于tableviewCell的布局内容能够阅读一下Apple的这篇文档:A Closer Look at Table View Cells
到此,咱们能够开始动手尝试编写该框架的一个初步实现的swift版本,其具备key-height缓存机制,暂无indexPath-height缓存机制和高度失效机制。
GitHub地址:TemplateLayoutCell
PS: 此项目只是做为学习该框架的一个playground~
欢迎你们指点,能点个💖就更棒啦~