FDTemplateLayoutCell源碼分析

FDTemplateLayoutCell 源碼分析

接口分析

對于FDTemplateLayoutCell這套代碼而言,接口設(shè)計比較簡單易用,所以我們首先對于其緩存接口分析,關(guān)于其他不重要的接口暫時略過。在heightForRowAtIndexPath中我們可以看到FDTemplateLayoutCell提供給用戶返回行高的方法:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    /*取出當(dāng)前用戶選擇的緩存方式*/
    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進行回調(diào)- (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中的模型復(fù)制抽取的一個方法。

內(nèi)部分析

fd_heightForCellWithIdentifier 內(nèi)部的實現(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ù)當(dāng)前Cell計算行高,并對計算結(jié)果根據(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 方法計算結(jié)果
    }

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

可以看到,當(dāng)傳入?yún)?shù)為需要緩存時,會先調(diào)用FDIndexPathHeightCache類的
- (BOOL)existsHeightAtIndexPath:(NSIndexPath *)indexPath
方法判斷傳入的indexPath是否已經(jīng)緩存過行高,如果有則直接取出;如果沒有,則進行下一步:調(diào)用
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration
方法進行行高計算。關(guān)于FDIndexPathHeightCache類和FDKeyedHeightCache類的緩存實現(xiàn)后面會具體介紹,現(xiàn)在繼續(xù)深入templateLayoutCell模塊的設(shè)計。

生成templateCell并根據(jù)這個Cell計算行高

- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration
在上面的方法中,identifier 和 block 都作為入?yún)ⅲ琤lock自然是作為回調(diào)使用,identifier的作用在接下來的過程中很重要。
**該方法調(diào)用fd_templateCellForReuseIdentifier返回一個UITableViewCell類型的cell。這個cell在后續(xù)被函數(shù)fd_systemFittingHeightForConfiguratedCell調(diào)用作為入?yún)⑦M行行高的計算。 **

好了,到現(xiàn)在我們大致知道這個框架到現(xiàn)在為止做了一些什么。

  1. 判斷是否需要緩存UITableViewCell的行高。
  2. 如果不需要,直接前往第四步。
  3. 根據(jù)indexPath或者key查找是否有計算并緩存過的行高,如果有,前往第五步。
  4. 生成templateCell,根據(jù)這個Cell和傳入的identifier進行行高的計算。
  5. 返回行高給控制器的heightForRow方法,如果需要緩存則根據(jù)對應(yīng)的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對應(yīng)的寬度*/
        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;

這個方法內(nèi)部主要邏輯如下:

  1. 首先判斷用戶是否使用frameLayout來進行界面的布局,如果不是,則默認(rèn)用戶使用autolayout進行布局,執(zhí)行第二步;如果是,則執(zhí)行第三步。
  2. 添加額外約束,然后使用- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize方法自動計算行高,如果fittingHeight返回0,則繼續(xù)執(zhí)行第三步。
  3. 使用重載方法- (CGSize)sizeThatFits:(CGSize)size手動計算行高。這個方法為作者自己實現(xiàn)重載的方法。
  4. 如果第三步仍然得不到結(jié)果,返回默認(rèn)行高44。

自動的緩存失效機制

無須擔(dān)心你數(shù)據(jù)源的變化引起的緩存失效,當(dāng)調(diào)用如-reloadData,-deleteRowsAtIndexPaths:withRowAnimation:等任何一個觸發(fā) UITableView 刷新機制的方法時,已有的高度緩存將以最小的代價執(zhí)行失效。如刪除一個 indexPath 為 [0:5] 的 cell 時,[0:0] ~ [0:4] 的高度緩存不受影響,而 [0:5] 后面所有的緩存值都向前移動一個位置。自動緩存失效機制對 UITableView 的 9 個公有 API 都進行了分別的處理,以保證沒有一次多余的高度計算。

FDTemplateCell中使用rumtime中的Method Swizzling將UITableView內(nèi)部的reloadDatainserSections:withRowAnimation:deleteRowsAtIndexPaths:withRowAnimation:等方法替換成自己的方法。從而達到以上敘述的目的。其實現(xiàn)方法比較容易明白,其實就是將緩存數(shù)組/字典中對應(yīng)indexPath的緩存進行操作,而不影響其他內(nèi)容。例如在deleteRowsAtIndexPaths:withRowAnimation:中:

    if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) {
        [self.fd_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:indexPaths];
        /*mutableIndexSetsToRemove中的key是section,對應(yīng)的值為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中刪除對應(yīng)需要刪除的row*/
                [heightsBySection[key.integerValue] removeObjectsAtIndexes:indexSet];
            }];
        }];
    }

關(guān)于使用RunLoop進行行高預(yù)緩存功能

作者的博客中有提到一套利用runloop進行行高預(yù)緩存的方法,但是似乎已經(jīng)被廢棄,而且在源碼中也沒有找到。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容