开源库UITableView+FDTemplateLayoutCell学习
摘自:优化UITableViewCell高度计算Swift版、优化UITableViewCell高度计算的那些事
本文带大家详细探索那篇文章所对应的库(1.2版),这个库就是利用缓存tableviewcell的高度提高滑动的流畅性。
主要是利用Runloop在空闲状态时,后台计算tableviewcell的高度并缓存起来。然后在使用的时候就直接从缓存中去,这里都放在一个数组里存在内存。
对Runloop以及几个mode不懂的可以看sunnyxx blog中的视频 视频可戳 , 文章的话可以看看 深入理解RunLoop、 【iOS程序启动与运转】- RunLoop个人小结。
其实就是在kCFRunLoopDefaultMode模式下BeforWaitting状态去执行计算的。
下面来探究源码。首先在UITableView+FDTemplateLayoutCell 下载源码,下载1.2版本。
然后你得到的库就只有两个文件:
.m文件大概只有500行代码。
下面看下作者的视线思路:
1. 创建了一个_FDTemplateLayoutCellHeightCache
类,就是管理Cache的一个类,里面有两个属性四个方法。
属性:
-
sections
这个变量就是用来存储缓存的height的一个二维数组。(因为tableview有section和row组成所以必须二维) -
_FDTemplateLayoutCellHeightCacheAbsentValue
这个是一个静态常量,就是用来标记没有缓存高度的row 。
方法:
buildHeightCachesAtIndexPathsIfNeeded:indexPaths
这个方法传入indexPaths数组来给sections中还没有初始化的元素进行初始化hasCachedHeightAtIndexPath:indexPath
根据下标索引判断是否有缓存(其实就是判断是否等于上面那个静态常量)cacheHeight:height:byIndexPath
根据indexPath给sections赋值。cachedHeightAtIndexPath:indexPath
根据indexPath取值
这个类主要是操作和存储缓存的。这个类的代码如下:
@interface _FDTemplateLayoutCellHeightCache : NSObject @property (nonatomic, strong) NSMutableArray *sections; @end static CGFloat const _FDTemplateLayoutCellHeightCacheAbsentValue = -1; @implementation _FDTemplateLayoutCellHeightCache - (void)buildHeightCachesAtIndexPathsIfNeeded:(NSArray *)indexPaths { if (indexPaths.count == 0) { return; } if (!self.sections) { self.sections = @[].mutableCopy; } // Build every section array or row array which is smaller than given index path. [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { for (NSInteger section = 0; section <= indexPath.section; ++section) { if (section >= self.sections.count) { self.sections[section] = @[].mutableCopy; } } NSMutableArray *rows = self.sections[indexPath.section]; for (NSInteger row = 0; row <= indexPath.row; ++row) { if (row >= rows.count) { rows[row] = @(_FDTemplateLayoutCellHeightCacheAbsentValue); } } }]; } - (BOOL)hasCachedHeightAtIndexPath:(NSIndexPath *)indexPath { [self buildHeightCachesAtIndexPathsIfNeeded:@[indexPath]]; NSNumber *cachedNumber = self.sections[indexPath.section][indexPath.row]; return ![cachedNumber isEqualToNumber:@(_FDTemplateLayoutCellHeightCacheAbsentValue)]; } - (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath { [self buildHeightCachesAtIndexPathsIfNeeded:@[indexPath]]; self.sections[indexPath.section][indexPath.row] = @(height); } - (CGFloat)cachedHeightAtIndexPath:(NSIndexPath *)indexPath { [self buildHeightCachesAtIndexPathsIfNeeded:@[indexPath]]; #if CGFLOAT_IS_DOUBLE return [self.sections[indexPath.section][indexPath.row] doubleValue]; #else return [self.sections[indexPath.section][indexPath.row] floatValue]; #endif } @end
2. 接下来是UITableView的一个扩展UITableView + FDTemplateLayoutCellPrivate
- 第一个方法fd_templateCellForReuseIdentifier:identifier,这个方法主要是通过你传入的一个identifier(就是复用的id)获取cell。
第一句是这样的 NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
OC中的 _cmd 代表的就是本方法,objc_getAssociatedObject
获取一个关联对象的属性。
- 接下来提供了一个方法来获取管理Cache的_FDTemplateLayoutCellHeightCache的对象
fd_cellHeightCache。
-
属性:fd_autoCacheInvalidationEnabled 记录是否自动缓存高度
-
属性:fd_precacheEnabled
这是一个私有类,下面给出这个类的完整代码:
@interface UITableView (FDTemplateLayoutCellPrivate) /// Returns a template cell created by reuse identifier, it has to be registered to table view. /// Lazy getter, and associated to table view. - (id)fd_templateCellForReuseIdentifier:(NSString *)identifier; /// A private height cache data structure. @property (nonatomic, strong, readonly) _FDTemplateLayoutCellHeightCache *fd_cellHeightCache; /// This is a private switch that I don't think caller should concern. /// Auto turn on when you use "-fd_heightForCellWithIdentifier:cacheByIndexPath:configuration". @property (nonatomic, assign) BOOL fd_autoCacheInvalidationEnabled; /// It helps to improve scroll performance by "pre-cache" height of cells that have not /// been displayed on screen. These calculation tasks are collected and performed only /// when "RunLoop" is in "idle" time. /// /// Auto turn on when you use "-fd_heightForCellWithIdentifier:cacheByIndexPath:configuration". @property (nonatomic, assign) BOOL fd_precacheEnabled; /// Debug log controlled by "fd_debugLogEnabled". - (void)fd_debugLog:(NSString *)message; @end @implementation UITableView (FDTemplateLayoutCellPrivate) - (id)fd_templateCellForReuseIdentifier:(NSString *)identifier { NSAssert(identifier.length > 0, @"Expects a valid identifier - %@", identifier); NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd); if (!templateCellsByIdentifiers) { templateCellsByIdentifiers = @{}.mutableCopy; objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } UITableViewCell *templateCell = templateCellsByIdentifiers[identifier]; if (!templateCell) { templateCell = [self dequeueReusableCellWithIdentifier:identifier]; NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier); templateCell.fd_isTemplateLayoutCell = YES; templateCellsByIdentifiers[identifier] = templateCell; [self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]]; } return templateCell; } - (_FDTemplateLayoutCellHeightCache *)fd_cellHeightCache { _FDTemplateLayoutCellHeightCache *cache = objc_getAssociatedObject(self, _cmd); if (!cache) { cache = [_FDTemplateLayoutCellHeightCache new]; objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN); } return cache; } - (BOOL)fd_autoCacheInvalidationEnabled { return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setFd_autoCacheInvalidationEnabled:(BOOL)enabled { objc_setAssociatedObject(self, @selector(fd_autoCacheInvalidationEnabled), @(enabled), OBJC_ASSOCIATION_RETAIN); } - (BOOL)fd_precacheEnabled { return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setFd_precacheEnabled:(BOOL)precacheEnabled { objc_setAssociatedObject(self, @selector(fd_precacheEnabled), @(precacheEnabled), OBJC_ASSOCIATION_RETAIN); } - (void)fd_debugLog:(NSString *)message { if (!self.fd_debugLogEnabled) { return; } NSLog(@"** FDTemplateLayoutCell ** %@", message); } @end
3. 下面又是一个分类,(这个是重点计算高度,调用缓存管理方法的分类)UITableView + FDTemplateLayoutCellPrecache
这个里面的方法在他blog中也有提到就是在NSDefaultRunLoopMode下当状态将要进入休眠的时候把计算方法分解成多个RunLoop Source任务(source0)
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
这个方法将创建一个 Source 0 任务,分发到指定线程的 RunLoop 中,在给定的 Mode 下执行,若指定的 RunLoop 处于休眠状态,则唤醒它处理事件.
主要逻辑就是先通过遍历所有section和row找到还没有缓存的row,然后加入到待缓存数组 ,创建一个observer去监听Runloop的状态 ,如果空闲了去创建source0任务,执行计算方法并缓存起来。如果预缓存任务完成了就把监听的Observer移除了。
下面给出这个类的代码:
@implementation UITableView (FDTemplateLayoutCellPrecache) - (void)fd_precacheIfNeeded { if (!self.fd_precacheEnabled) { return; } // Delegate could use "rowHeight" rather than implements this method. if (![self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) { return; } CFRunLoopRef runLoop = CFRunLoopGetCurrent(); // This is a idle mode of RunLoop, when UIScrollView scrolls, it jumps into "UITrackingRunLoopMode" // and won't perform any cache task to keep a smooth scroll. CFStringRef runLoopMode = kCFRunLoopDefaultMode; // Collect all index paths to be precached. NSMutableArray *mutableIndexPathsToBePrecached = self.fd_allIndexPathsToBePrecached.mutableCopy; // Setup a observer to get a perfect moment for precaching tasks. // We use a "kCFRunLoopBeforeWaiting" state to keep RunLoop has done everything and about to sleep // (mach_msg_trap), when all tasks finish, it will remove itself. CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler (kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) { // Remove observer when all precache tasks are done. if (mutableIndexPathsToBePrecached.count == 0) { CFRunLoopRemoveObserver(runLoop, observer, runLoopMode); return; } // Pop first index path record as this RunLoop iteration's task. NSIndexPath *indexPath = mutableIndexPathsToBePrecached.firstObject; [mutableIndexPathsToBePrecached removeObject:indexPath]; // This method creates a "source 0" task in "idle" mode of RunLoop, and will be // performed in a future RunLoop iteration only when user is not scrolling. [self performSelector:@selector(fd_precacheIndexPathIfNeeded:) onThread:[NSThread mainThread] withObject:indexPath waitUntilDone:NO modes:@[NSDefaultRunLoopMode]]; }); CFRunLoopAddObserver(runLoop, observer, runLoopMode); } - (void)fd_precacheIndexPathIfNeeded:(NSIndexPath *)indexPath { if (![self.fd_cellHeightCache hasCachedHeightAtIndexPath:indexPath]) { CGFloat height = [self.delegate tableView:self heightForRowAtIndexPath:indexPath]; [self.fd_cellHeightCache cacheHeight:height byIndexPath:indexPath]; [self fd_debugLog:[NSString stringWithFormat: @"precached - [%@:%@] %@", @(indexPath.section), @(indexPath.row), @(height)]]; } } - (NSArray *)fd_allIndexPathsToBePrecached { NSMutableArray *allIndexPaths = @[].mutableCopy; for (NSInteger section = 0; section < [self numberOfSections]; ++section) { for (NSInteger row = 0; row < [self numberOfRowsInSection:section]; ++row) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; if (![self.fd_cellHeightCache hasCachedHeightAtIndexPath:indexPath]) { [allIndexPaths addObject:indexPath]; } } } return allIndexPaths.copy; } @end
4. 下面又是一个分类UITableView + FDTemplateLayoutCellAutomaticallyCacheInvalidation
因为我们会有一些操作导致cell的改变,所以这里作者要保证在每次cell改变的时候把sections数组改掉,然后如果新增或者修改了 需要重新计算高度。用到了methodSwizzle 黑魔法。这里作者把swizzle放在了UITableView的load类方法中。需要使用methodSwizzle的方法有:
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:)
};
这个类的代码:
@implementation UITableView (FDTemplateLayoutCellAutomaticallyCacheInvalidation) + (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); } } - (void)fd_reloadData { if (self.fd_autoCacheInvalidationEnabled) { [self.fd_cellHeightCache.sections removeAllObjects]; } [self fd_reloadData]; // Primary call [self fd_precacheIfNeeded]; } - (void)fd_insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { if (self.fd_autoCacheInvalidationEnabled) { [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { [self.fd_cellHeightCache.sections insertObject:@[].mutableCopy atIndex:idx]; }]; } [self fd_insertSections:sections withRowAnimation:animation]; // Primary call [self fd_precacheIfNeeded]; } - (void)fd_deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { if (self.fd_autoCacheInvalidationEnabled) { [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { [self.fd_cellHeightCache.sections removeObjectAtIndex:idx]; }]; } [self fd_deleteSections:sections withRowAnimation:animation]; // Primary call } - (void)fd_reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { if (self.fd_autoCacheInvalidationEnabled) { [sections enumerateIndexesUsingBlock: ^(NSUInteger idx, BOOL *stop) { NSMutableArray *rows = self.fd_cellHeightCache.sections[idx]; for (NSInteger row = 0; row < rows.count; ++row) { rows[row] = @(_FDTemplateLayoutCellHeightCacheAbsentValue); } }]; } [self fd_reloadSections:sections withRowAnimation:animation]; // Primary call [self fd_precacheIfNeeded]; } - (void)fd_moveSection:(NSInteger)section toSection:(NSInteger)newSection { if (self.fd_autoCacheInvalidationEnabled) { [self.fd_cellHeightCache.sections exchangeObjectAtIndex:section withObjectAtIndex:newSection]; } [self fd_moveSection:section toSection:newSection]; // Primary call } - (void)fd_insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { if (self.fd_autoCacheInvalidationEnabled) { [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { NSMutableArray *rows = self.fd_cellHeightCache.sections[indexPath.section]; [rows insertObject:@(_FDTemplateLayoutCellHeightCacheAbsentValue) atIndex:indexPath.row]; }]; } [self fd_insertRowsAtIndexPaths:indexPaths withRowAnimation:animation]; // Primary call [self fd_precacheIfNeeded]; } - (void)fd_deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { if (self.fd_autoCacheInvalidationEnabled) { [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { [self.fd_cellHeightCache.sections[indexPath.section] removeObjectAtIndex:indexPath.row]; }]; } [self fd_deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation]; // Primary call } - (void)fd_reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { if (self.fd_autoCacheInvalidationEnabled) { [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { NSMutableArray *rows = self.fd_cellHeightCache.sections[indexPath.section]; rows[indexPath.row] = @(_FDTemplateLayoutCellHeightCacheAbsentValue); }]; } [self fd_reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation]; // Primary call [self fd_precacheIfNeeded]; } - (void)fd_moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { if (self.fd_autoCacheInvalidationEnabled) { NSMutableArray *sourceRows = self.fd_cellHeightCache.sections[sourceIndexPath.section]; NSMutableArray *destinationRows = self.fd_cellHeightCache.sections[destinationIndexPath.section]; NSNumber *sourceValue = sourceRows[sourceIndexPath.row]; NSNumber *destinationValue = destinationRows[destinationIndexPath.row]; sourceRows[sourceIndexPath.row] = destinationValue; destinationRows[destinationIndexPath.row] = sourceValue; } [self fd_moveRowAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; // Primary call } @end
5. 下面还有一个分类UITableView + FDTemplateLayoutCell,这个类提供外界获取cell高度的方法
- fd_heightForCellWithIdentifier:configuration:configuration
- fd_heightForCellWithIdentifier:cacheByIndexPath:configuration:configuration
这个类的方法如下:
@implementation UITableView (FDTemplateLayoutCell) - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id))configuration { if (!identifier) { return 0; } // Fetch a cached template cell for `identifier`. UITableViewCell *cell = [self fd_templateCellForReuseIdentifier:identifier]; // Manually calls to ensure consistent behavior with actual cells (that are displayed on screen). [cell prepareForReuse]; // Customize and provide content for our template cell. if (configuration) { configuration(cell); } // Add a hard width constraint to make dynamic content views (like labels) expand vertically instead // of growing horizontally, in a flow-layout manner. NSLayoutConstraint *tempWidthConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:CGRectGetWidth(self.frame)]; [cell.contentView addConstraint:tempWidthConstraint]; // Auto layout engine does its math CGSize fittingSize = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; [cell.contentView removeConstraint:tempWidthConstraint]; // Add 1px extra space for separator line if needed, simulating default UITableViewCell. if (self.separatorStyle != UITableViewCellSeparatorStyleNone) { fittingSize.height += 1.0 / [UIScreen mainScreen].scale; } return fittingSize.height; } - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id))configuration { if (!identifier || !indexPath) { return 0; } // Enable auto cache invalidation if you use this "cacheByIndexPath" API. if (!self.fd_autoCacheInvalidationEnabled) { self.fd_autoCacheInvalidationEnabled = YES; } // Enable precache if you use this "cacheByIndexPath" API. if (!self.fd_precacheEnabled) { self.fd_precacheEnabled = YES; // Manually trigger precache only for the first time. [self fd_precacheIfNeeded]; } // Hit the cache if ([self.fd_cellHeightCache hasCachedHeightAtIndexPath:indexPath]) { [self fd_debugLog:[NSString stringWithFormat: @"hit cache - [%@:%@] %@", @(indexPath.section), @(indexPath.row), @([self.fd_cellHeightCache cachedHeightAtIndexPath:indexPath])]]; return [self.fd_cellHeightCache cachedHeightAtIndexPath:indexPath]; } // Do calculations CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration]; [self fd_debugLog:[NSString stringWithFormat: @"calculate - [%@:%@] %@", @(indexPath.section), @(indexPath.row), @(height)]]; // Cache it [self.fd_cellHeightCache cacheHeight:height byIndexPath:indexPath]; return height; } - (BOOL)fd_debugLogEnabled { return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setFd_debugLogEnabled:(BOOL)debugLogEnabled { objc_setAssociatedObject(self, @selector(fd_debugLogEnabled), @(debugLogEnabled), OBJC_ASSOCIATION_RETAIN); } @end