FDTemplateLayoutCell 源碼分析
接口分析
對于FDTemplateLayoutCell這套代碼而言嘉汰,接口設計比較簡單易用谱净,所以我們首先對于其緩存接口分析浩聋,關于其他不重要的接口暫時略過颅拦。在heightForRowAtIndexPath
中我們可以看到FDTemplateLayoutCell提供給用戶返回行高的方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
/*取出當前用戶選擇的緩存方式*/
FDSimulatedCacheMode mode = self.cacheModeSegmentControl.selectedSegmentIndex;
switch (mode) {
/*無緩存*/
case FDSimulatedCacheModeNone:
return [tableView fd_heightForCellWithIdentifier:@"FDFeedCell" configuration:^(FDFeedCell *cell) {
[self configureCell:cell atIndexPath:indexPath];
}];
/*根據(jù)indexPath緩存*/
case FDSimulatedCacheModeCacheByIndexPath:
return [tableView fd_heightForCellWithIdentifier:@"FDFeedCell" cacheByIndexPath:indexPath configuration:^(FDFeedCell *cell) {
[self configureCell:cell atIndexPath:indexPath];
}];
/*根據(jù)模型的key緩存*/
case FDSimulatedCacheModeCacheByKey: {
FDFeedEntity *entity = self.feedEntitySections[indexPath.section][indexPath.row];
return [tableView fd_heightForCellWithIdentifier:@"FDFeedCell" cacheByKey:entity.identifier configuration:^(FDFeedCell *cell) {
[self configureCell:cell atIndexPath:indexPath];
}];
};
default:
break;
}
}
注意到,每一個方法都傳入了一個名為configuration
的block進行回調- (void)configureCell:(FDFeedCell *)cell atIndexPath:(NSIndexPath *)indexPath
方法怖侦。 這個方法的具體實現(xiàn)如下:
- (void)configureCell:(FDFeedCell *)cell atIndexPath:(NSIndexPath *)indexPath {
cell.fd_enforceFrameLayout = NO; // Enable to use "-sizeThatFits:"
if (indexPath.row % 2 == 0) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
} else {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
cell.entity = self.feedEntitySections[indexPath.section][indexPath.row];
}
實際上這是對Cell中的模型復制抽取的一個方法篡悟。
內部分析
fd_heightForCellWithIdentifier
內部的實現(xiàn)邏輯可以使用以下代碼進行表示:
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheBySomeMehthod:(id *)methodId configuration:(void (^)(id cell))configuration {
//傳入一種緩存方式
//這里的methodId 可以是key 或者 indexPath
//接下來以indexPath為例
if method != none{
if (!identifier || !indexPath) {
return 0;
}
// Hit cache
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];
}
/*緩存未命中,則計算根據(jù)當前Cell計算行高搬葬,并對計算結果根據(jù)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)]];
}else{/*如果不需要緩存行高 則直接生成templateLayoutCell 利用templateLayoutCell計算行高后返回*/
height = fd_heightForCellWithIdentifier 方法計算結果
}
return height;
}
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration {
if (!identifier) {
return 0;
}
UITableViewCell *templateLayoutCell = [self fd_templateCellForReuseIdentifier:identifier];
// Manually calls to ensure consistent behavior with actual cells. (that are displayed on screen)
[templateLayoutCell prepareForReuse];
// Customize and provide content for our template cell.
if (configuration) {
configuration(templateLayoutCell);
}
return [self fd_systemFittingHeightForConfiguratedCell:templateLayoutCell];
}
可以看到荷腊,當傳入?yún)?shù)為需要緩存時,會先調用FDIndexPathHeightCache
類的
- (BOOL)existsHeightAtIndexPath:(NSIndexPath *)indexPath
方法判斷傳入的indexPath是否已經緩存過行高急凰,如果有則直接取出女仰;如果沒有,則進行下一步:調用
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration
方法進行行高計算抡锈。關于FDIndexPathHeightCache
類和FDKeyedHeightCache
類的緩存實現(xiàn)后面會具體介紹疾忍,現(xiàn)在繼續(xù)深入templateLayoutCell
模塊的設計。
生成templateCell并根據(jù)這個Cell計算行高
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration
在上面的方法中床三,identifier 和 block 都作為入?yún)⒁徽郑琤lock自然是作為回調使用,identifier的作用在接下來的過程中很重要撇簿。
**該方法調用fd_templateCellForReuseIdentifier
返回一個UITableViewCell類型的cell聂渊。這個cell在后續(xù)被函數(shù)fd_systemFittingHeightForConfiguratedCell
調用作為入?yún)⑦M行行高的計算。 **
好了补疑,到現(xiàn)在我們大致知道這個框架到現(xiàn)在為止做了一些什么歧沪。
- 判斷是否需要緩存UITableViewCell的行高歹撒。
- 如果不需要莲组,直接前往第四步。
- 根據(jù)indexPath或者key查找是否有計算并緩存過的行高暖夭,如果有锹杈,前往第五步。
- 生成templateCell迈着,根據(jù)這個Cell和傳入的identifier進行行高的計算竭望。
- 返回行高給控制器的
heightForRow
方法,如果需要緩存則根據(jù)對應的indexPath或者key進行緩存裕菠。
行高計算
fd_systemFittingHeightForConfiguratedCell
方法中templateCell作為入?yún)⑴c行高計算咬清,而不是用于顯示在屏幕上。
#pragma mark 計算Cell的行高
- (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell {
CGFloat contentViewWidth = CGRectGetWidth(self.frame);
CGRect cellBounds = cell.bounds;
cellBounds.size.width = contentViewWidth;
cell.bounds = cellBounds;
CGFloat accessroyWidth = 0;
// 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) {
accessroyWidth = 16 + CGRectGetWidth(cell.accessoryView.frame);
} else {
/*生成一個字典存儲不同accessoryType對應的寬度*/
static const CGFloat systemAccessoryWidths[] = {
[UITableViewCellAccessoryNone] = 0,
[UITableViewCellAccessoryDisclosureIndicator] = 34,
[UITableViewCellAccessoryDetailDisclosureButton] = 68,
[UITableViewCellAccessoryCheckmark] = 40,
[UITableViewCellAccessoryDetailButton] = 48
};
accessroyWidth = systemAccessoryWidths[cell.accessoryType];
}
contentViewWidth -= accessroyWidth;
// 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.
//以下添加約束的代碼省略
// Auto layout engine does its math
fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
}
if (fittingHeight == 0) {
// 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;
這個方法內部主要邏輯如下:
- 首先判斷用戶是否使用frameLayout來進行界面的布局奴潘,如果不是旧烧,則默認用戶使用autolayout進行布局,執(zhí)行第二步画髓;如果是掘剪,則執(zhí)行第三步。
- 添加額外約束奈虾,然后使用
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize
方法自動計算行高夺谁,如果fittingHeight
返回0廉赔,則繼續(xù)執(zhí)行第三步。 - 使用重載方法
- (CGSize)sizeThatFits:(CGSize)size
手動計算行高匾鸥。這個方法為作者自己實現(xiàn)重載的方法蜡塌。 - 如果第三步仍然得不到結果,返回默認行高44扫腺。
自動的緩存失效機制
無須擔心你數(shù)據(jù)源的變化引起的緩存失效岗照,當調用如-reloadData,-deleteRowsAtIndexPaths:withRowAnimation:等任何一個觸發(fā) UITableView 刷新機制的方法時笆环,已有的高度緩存將以最小的代價執(zhí)行失效攒至。如刪除一個 indexPath 為 [0:5] 的 cell 時,[0:0] ~ [0:4] 的高度緩存不受影響躁劣,而 [0:5] 后面所有的緩存值都向前移動一個位置迫吐。自動緩存失效機制對 UITableView 的 9 個公有 API 都進行了分別的處理,以保證沒有一次多余的高度計算账忘。
FDTemplateCell中使用rumtime中的Method Swizzling將UITableView
內部的reloadData
志膀、inserSections:withRowAnimation:
、deleteRowsAtIndexPaths:withRowAnimation:
等方法替換成自己的方法鳖擒。從而達到以上敘述的目的溉浙。其實現(xiàn)方法比較容易明白,其實就是將緩存數(shù)組/字典中對應indexPath
的緩存進行操作蒋荚,而不影響其他內容戳稽。例如在deleteRowsAtIndexPaths:withRowAnimation:
中:
if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) {
[self.fd_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:indexPaths];
/*mutableIndexSetsToRemove中的key是section,對應的值為row*/
NSMutableDictionary<NSNumber *, NSMutableIndexSet *> *mutableIndexSetsToRemove = [NSMutableDictionary dictionary];
[indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
NSMutableIndexSet *mutableIndexSet = mutableIndexSetsToRemove[@(indexPath.section)];
if (!mutableIndexSet) {
mutableIndexSet = [NSMutableIndexSet indexSet]; // return indexSet with no members
mutableIndexSetsToRemove[@(indexPath.section)] = mutableIndexSet;
}
[mutableIndexSet addIndex:indexPath.row];
}];
[mutableIndexSetsToRemove enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, NSIndexSet *indexSet, BOOL *stop) {
[self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) {
/*heightsBySection就是緩存行高用的數(shù)組*/
/*typedef NSMutableArray<NSMutableArray<NSNumber *> *> FDIndexPathHeightsBySection;*/
/*在每個section中刪除對應需要刪除的row*/
[heightsBySection[key.integerValue] removeObjectsAtIndexes:indexSet];
}];
}];
}
關于使用RunLoop進行行高預緩存功能
在作者的博客中有提到一套利用runloop進行行高預緩存的方法期升,但是似乎已經被廢棄惊奇,而且在源碼中也沒有找到。