開源庫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
開源庫UITableView+FDTemplateLayoutCell學習