FDTemplateLayoutCell走读

FDTemplateLayoutCell 代码走读

最近组内有人使用了这个tableview算cell高库,一看都是自动算高的,让人感觉很不错,故走读代码,研究其具体运行方式。

框架地址

作者之一

首先在博客中提到的使用RunLoop空闲时间执行预缓存任务,这一块代码并没有找到,估计是我看的这个版本已经去掉了这个功能了。

- UITableView+FDIndexPathHeightCache.h
- UITableView+FDIndexPathHeightCache.m
- UITableView+FDKeyedHeightCache.h
- UITableView+FDKeyedHeightCache.m
- UITableView+FDTemplateLayoutCell.h
- UITableView+FDTemplateLayoutCell.m
- UITableView+FDTemplateLayoutCellDebug.h
- UITableView+FDTemplateLayoutCellDebug.m

我重点关注的几个类

UITableView+FDIndexPathHeightCache 这里是通过NSIndexPath来存储算出来的高的,简单说就是创建了一个二维数组,将数据进行了填充。

UITableView+FDKeyedHeightCache 这里是cell通过key值进行缓存高度的类,和上面实现的功能完全一样,但是做法不同,就是要求每个cell都提供一个identifier来充当key值,这个就比较简单了。事实上这一块代码量并不多。

UITableView+FDTemplateLayoutCell 这里提供计算高度的方法,以及对外的接口,以及封装使用哪种缓存的方式。

了解这个框架,只需要关注几个点,1.高度是怎样算出来的,2,高度是怎样缓存的

高度算出的方法主要是在这里:

- (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell;

关键代码是:

if (cell.accessoryView) {
accessroyWidth = 16 + CGRectGetWidth(cell.accessoryView.frame);
} else {
static const CGFloat systemAccessoryWidths[] = {
[UITableViewCellAccessoryNone] = 0,
[UITableViewCellAccessoryDisclosureIndicator] = 34,
[UITableViewCellAccessoryDetailDisclosureButton] = 68,
[UITableViewCellAccessoryCheckmark] = 40,
[UITableViewCellAccessoryDetailButton] = 48
};
accessroyWidth = systemAccessoryWidths[cell.accessoryType];
}
contentViewWidth -= accessroyWidth;

以及

fittingHeight = [cell sizeThatFits:CGSizeMake(contentViewWidth, 0)].height;

在iOS10.2以上还需要将cell的contentView进行一次强制edge布局,便于最后能够正确的调用sizeThatFits得出高度。

但是,事实上,高度只需要算出一次就可以缓存起来,是不需要反复运算的,所以这里出现了存储的两种方式,1.使用indexpaht,2.使用key-value.这里使用indexpaht比较复杂,主要分为如下几个步骤 预先:首先生成横屏和竖屏两个数组, 获取,赋值:使用这三个方法:

- (BOOL)existsHeightAtIndexPath:(NSIndexPath *)indexPath {
[self buildCachesAtIndexPathsIfNeeded:@[indexPath]];
NSNumber *number = self.heightsBySectionForCurrentOrientation[indexPath.section][indexPath.row];
return ![number isEqualToNumber:@-1];
}

- (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath {
self.automaticallyInvalidateEnabled = YES;//save
[self buildCachesAtIndexPathsIfNeeded:@[indexPath]];
self.heightsBySectionForCurrentOrientation[indexPath.section][indexPath.row] = @(height);
}

- (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
}

代码不难懂,这里需要注意的是,每次都要进行一次[self buildCachesAtIndexPathsIfNeeded:@[indexPath]],这个就是创建占位数据-1,具体就是实时的创建对应cell的占位数据(在二维数组中生成对应的占位数据):

- (void)buildSectionsIfNeeded:(NSInteger)targetSection {
[self enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) {
for (NSInteger section = 0; section <= targetSection; ++section) {
if (section >= heightsBySection.count) {
heightsBySection[section] = [NSMutableArray array];
}
}
}];
}

- (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;
}
}
}];
}

通过上面这五个函数,数据就已经做到了缓存了,建表,缓存,获取,事实上我们的tableview可能会经常有变化的,用户删除了cell,那么我们对应的算高也要更新,这里又要更新一波二维数组,实际的做法是,利用runtime拦截系统方法。

+ (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);
}
}

看到没,这些系统方法已经被替换了前缀为fd_的对应函数了。

比如说

- (void)fd_reloadData {
if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) {
[self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) {
[heightsBySection removeAllObjects];
}];
}
FDPrimaryCall([self fd_reloadData];);
}

这里就是reloadData方法触发时调用,将横竖屏的数据前部清空。 再比如:

- (void)fd_insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation {
if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) {
[sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL *stop) {
[self.fd_indexPathHeightCache buildSectionsIfNeeded:section];
[self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) {
[heightsBySection insertObject:[NSMutableArray array] atIndex:section];
}];
}];
}
FDPrimaryCall([self fd_insertSections:sections withRowAnimation:animation];);
}

这里就是将insert方法替换,先在对于的二维数组中插入可变数组。然后再在数组中填充高度。

这就是使用二维数组的整个逻辑。

使用key-value的形式就更加简单了:

- (BOOL)existsHeightForKey:(id<NSCopying>)key {
NSNumber *number = self.mutableHeightsByKeyForCurrentOrientation[key];
return number && ![number isEqualToNumber:@-1];
}

- (void)cacheHeight:(CGFloat)height byKey:(id<NSCopying>)key {
self.mutableHeightsByKeyForCurrentOrientation[key] = @(height);
}

- (CGFloat)heightForKey:(id<NSCopying>)key {
#if CGFLOAT_IS_DOUBLE
return [self.mutableHeightsByKeyForCurrentOrientation[key] doubleValue];
#else
return [self.mutableHeightsByKeyForCurrentOrientation[key] floatValue];
#endif
}

- (void)invalidateHeightForKey:(id<NSCopying>)key {
[self.mutableHeightsByKeyForPortrait removeObjectForKey:key];
[self.mutableHeightsByKeyForLandscape removeObjectForKey:key];
}

- (void)invalidateAllHeightCache {
[self.mutableHeightsByKeyForPortrait removeAllObjects];
[self.mutableHeightsByKeyForLandscape removeAllObjects];
}

由于不存在删除key的情况,所以不需要考虑拦截系统操作。

最近的文章

给博客地址Http协议加个s

由于域名到期,目前我的blog将暂时使用http协议。这篇文章,是我去年做到https留下的记录,确实可以做到文中所述。让人无奈的运营商偶然间发现打开博客,弹出了一个底部toast广告,虽然只有短短数秒,但显示内容居然是18禁的东西,我秉着认真负责的态度,为广大读者以身试法点开了,居然要某快播变种软件,下载后发现居然是exe,没有Mac对应平台!这种打着发福利变相谋私利的禽兽,真的是太让人生气了。所以我决定,给自己的博客加个s,保证不在传输过程中被添油加醋。GitHub Pages最近已经...…

继续阅读
更早的文章

iOS下NavigationBar解析

iOS下NavigationBar解析最近在制作基于毛玻璃的一个滑动渐变NavigationBar,对NavigationBar的层级有了一些了解,故做了一个小的总结NavigationBar这是一个基本的层级:可以看到,UINavigationBar有一个控制自身颜色的UIBarBackground,与它同级的有两个按钮,done,title等按钮,我们关注的就是UIBarBackgroundUIBarBackgroundUIImageView其实就是shadowImage,就是Navi...…

继续阅读