1.入口
//indexPath缓存入口
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration {
if (!identifier || !indexPath) {
return 0;
}
// 有高度缓存,直接获取
if ([self.fd_indexPathHeightCache existsHeightAtIndexPath:indexPath]) {
[self fd_debugLog:[NSString stringWithFormat:@"hit cache by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @([self.fd_indexPathHeightCache heightForIndexPath:indexPath])]];
return [self.fd_indexPathHeightCache heightForIndexPath:indexPath];
}
//没有高度缓存,计算后放入缓存
CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
[self.fd_indexPathHeightCache cacheHeight:height byIndexPath:indexPath];
[self fd_debugLog:[NSString stringWithFormat: @"cached by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @(height)]];
return height;
}
- 非空判断,identifier或indexPath为空,返回0。
- indexPath对应的高度是否缓存?有缓存,取缓存高度返回。
- 无缓存,计算缓存高度,保存后并返回。
2.判断高度缓存是否存在
- (BOOL)existsHeightAtIndexPath:(NSIndexPath *)indexPath {
[self buildCachesAtIndexPathsIfNeeded:@[indexPath]];
NSNumber *number = self.heightsBySectionForCurrentOrientation[indexPath.section][indexPath.row];
return ![number isEqualToNumber:@-1];
}
#pragma mark - 初始化
- (void)buildCachesAtIndexPathsIfNeeded:(NSArray *)indexPaths {
// Build every section array or row array which is smaller than given index path.
[indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
[self buildSectionsIfNeeded:indexPath.section];
[self buildRowsIfNeeded:indexPath.row inExistSection:indexPath.section];
}];
}
//section赋值为数组
- (void)buildSectionsIfNeeded:(NSInteger)targetSection {
[self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) {
for (NSInteger section = 0; section <= targetSection; ++section) {
if (section >= heightsBySection.count) {
heightsBySection[section] = [NSMutableArray array];
}
}
}];
}
//初始高度-1
- (void)buildRowsIfNeeded:(NSInteger)targetRow inExistSection:(NSInteger)section {
[self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) {
NSMutableArray<NSNumber *> *heightsByRow = heightsBySection[section];
for (NSInteger row = 0; row <= targetRow; ++row) {
if (row >= heightsByRow.count) {
heightsByRow[row] = @-1;
}
}
}];
}
- buildCachesAtIndexPathsIfNeeded:初始化section数组,初始化section数组中的值即row的值为-1。
- heightsBySectionForCurrentOrientation是一个二维数组,根据横屏、竖屏对应两个数组。初始值为-1。
- 如果值不为-1,则表示indexPath对应的高度已经缓存。
typedef NSMutableArray<NSMutableArray<NSNumber *> *> FDIndexPathHeightsBySection;
- (FDIndexPathHeightsBySection *)heightsBySectionForCurrentOrientation {
return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation) ? self.heightsBySectionForPortrait: self.heightsBySectionForLandscape;
}
3.获取缓存高度
- (CGFloat)heightForIndexPath:(NSIndexPath *)indexPath {
[self buildCachesAtIndexPathsIfNeeded:@[indexPath]];
NSNumber *number = self.heightsBySectionForCurrentOrientation[indexPath.section][indexPath.row];
#if CGFLOAT_IS_DOUBLE
return number.doubleValue;
#else
return number.floatValue;
#endif
}
直接调用heightsBySectionForCurrentOrientation,读取横屏或竖屏对应数组里的值。
4.计算cell高度
- (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell {
CGFloat contentViewWidth = CGRectGetWidth(self.frame);
CGRect cellBounds = cell.bounds;
cellBounds.size.width = contentViewWidth;
cell.bounds = cellBounds;
CGFloat rightSystemViewsWidth = 0.0;
for (UIView *view in self.subviews) {
if ([view isKindOfClass:NSClassFromString(@"UITableViewIndex")]) {
rightSystemViewsWidth = CGRectGetWidth(view.frame);
break;
}
}
// 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];
}
if ([UIScreen mainScreen].scale >= 3 && [UIScreen mainScreen].bounds.size.width >= 414) {
rightSystemViewsWidth += 4;
}
contentViewWidth -= rightSystemViewsWidth;
// 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)]];
}
//计算自定义sizeThatFits高度
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)]];
}
// 如果约束没有,自定义也没有,给个默认高度
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;
}
- fd_enforceFrameLayout为NO时,计算约束高度。systemLayoutSizeFittingSize,计算contentView高度。
1.1 计算约束这段根据contentViewWidth算出widthFenceConstraint约束,添加到contentView.
1.2 中间有段是10.2以后系统,必须计算出上下左右的约束才能正确估算
1.3 计算之后需要移除
[cell.contentView removeConstraint:widthFenceConstraint];
if (isSystemVersionEqualOrGreaterThen10_2) {
[cell removeConstraints:edgeConstraints];
}
- 如果约束高度为0,则计算sizeThatFits方法中自定义的高度。
- 如果1、2计算的高度是0,则高度默认44.
- 最后添加一个像素
5.缓存高度
- (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath {
self.automaticallyInvalidateEnabled = YES;//c不理解
[self buildCachesAtIndexPathsIfNeeded:@[indexPath]];
self.heightsBySectionForCurrentOrientation[indexPath.section][indexPath.row] = @(height);
}
1.buildCachesAtIndexPathsIfNeeded如果没有初始化,此方法会初始化。
2.高度赋值给heightsBySectionForCurrentOrientation
6.reloadData
6.1方法交换
+ (void)load {
// All methods that trigger height cache's invalidation
SEL selectors[] = {
@selector(reloadData),
@selector(insertSections:withRowAnimation:),
@selector(deleteSections:withRowAnimation:),
@selector(reloadSections:withRowAnimation:),
@selector(moveSection:toSection:),
@selector(insertRowsAtIndexPaths:withRowAnimation:),
@selector(deleteRowsAtIndexPaths:withRowAnimation:),
@selector(reloadRowsAtIndexPaths:withRowAnimation:),
@selector(moveRowAtIndexPath:toIndexPath:)
};
for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
SEL originalSelector = selectors[index];
SEL swizzledSelector = NSSelectorFromString([@"fd_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
所有tableView的刷新、插入、删除等都通过runtime交换方法,添加fd_前缀来自定义操作。
6.2 reloadData
- (void)fd_reloadData {
if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) {
[self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) {
[heightsBySection removeAllObjects];
}];
}
FDPrimaryCall([self fd_reloadData];);
}
1.刷新操作都会移除所有的高度缓存。也就是说,所谓的高度缓存,并不是model发生改变时去重新计算高度,因为这个属性改变是否影响高度并不好掌握。
2.高度缓存是针对滚动时避免重复计算高度。
3.执行reloadData即视为数据源发生了变化,则高度重新计算。
6.3 FDPrimaryCall
static void __FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(void (^callout)(void)) {
callout();
}
#define FDPrimaryCall(...) do {__FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(^{__VA_ARGS__});} while(0)
FDPrimaryCall静态全局的block,回调了方法本身。这是为什么呢?
在MJRefresh中有这么段代码:
@implementation UITableView (MJRefresh)
+ (void)load
{
[self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
}
- (void)mj_reloadData
{
[self mj_reloadData];
[self executeReloadDataBlock];
}
@end
同时进行交换同一个方法,结果会怎么样呢?尝试直接调用[self fd_reloadData];偶现递归了,do {...} while(0)保证每次刷新只调用一次。
框架的作者发现了这个问题,已添加了注释。可能也是迫于无奈,很多框架都去交换reloadData。
// We just forward primary call, in crash report, top most method in stack maybe FD's,
// but it's really not our bug, you should check whether your table view's data source and
// displaying cells are not matched when reloading.
6.4 [self fd_reloadData]为什么会偶现递归?
递归信息- MJRefresh、FDTemplateLayoutCell同时使用了UITableView的category,并且同时交换了reloadData方法。实际情况不会产生递归。
- 具体的交换后执行顺序请参考这篇文章:iOS 多个category同时交换同一个方法
交换过程与结果 - 由图可知,调用reloadData顺序是:fd_reloadData->mj_reloadData->reloadData。
- 正常情况下,并不会产生递归,偶现过+(void)load多执行了一次,导致出现图中的crash。 真正的原因:注意系统库的坑之load函数调用多次,可用dispatch_once确保交换只有一次。
dispatch_once(&onceToken, ^{
<#code to be executed once#>
});
总结:
- UITableView+FDIndexPathHeightCache中藏着FDIndexPathHeightCache类,UITableView+FDKeyedHeightCache藏着FDKeyedHeightCache。感觉作者把类单独作为一个文件,暴露出来更好理解。
- 高度计算时,用代码布局只需要在sizeThatFits计算高度即可。这里有个demo,可供参考。
- 使用第三方就要了解第三方的思路,开发中遇到问题可以从根本上解决问题。
网友评论