FDTemplateLayoutCell源碼分析

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)在為止做了一些什么歧沪。

  1. 判斷是否需要緩存UITableViewCell的行高歹撒。
  2. 如果不需要莲组,直接前往第四步。
  3. 根據(jù)indexPath或者key查找是否有計算并緩存過的行高暖夭,如果有锹杈,前往第五步。
  4. 生成templateCell迈着,根據(jù)這個Cell和傳入的identifier進行行高的計算竭望。
  5. 返回行高給控制器的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;

這個方法內部主要邏輯如下:

  1. 首先判斷用戶是否使用frameLayout來進行界面的布局奴潘,如果不是旧烧,則默認用戶使用autolayout進行布局,執(zhí)行第二步画髓;如果是掘剪,則執(zhí)行第三步。
  2. 添加額外約束奈虾,然后使用- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize方法自動計算行高夺谁,如果fittingHeight返回0廉赔,則繼續(xù)執(zhí)行第三步。
  3. 使用重載方法- (CGSize)sizeThatFits:(CGSize)size手動計算行高匾鸥。這個方法為作者自己實現(xiàn)重載的方法蜡塌。
  4. 如果第三步仍然得不到結果,返回默認行高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進行行高預緩存的方法期升,但是似乎已經被廢棄惊奇,而且在源碼中也沒有找到。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末播赁,一起剝皮案震驚了整個濱河市颂郎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌容为,老刑警劉巖乓序,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異坎背,居然都是意外死亡替劈,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門沼瘫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抬纸,“玉大人,你說我怎么就攤上這事耿戚∈剩” “怎么了阿趁?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長坛猪。 經常有香客問我脖阵,道長,這世上最難降的妖魔是什么墅茉? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任命黔,我火速辦了婚禮,結果婚禮上就斤,老公的妹妹穿的比我還像新娘悍募。我一直安慰自己,他們只是感情好洋机,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布坠宴。 她就那樣靜靜地躺著,像睡著了一般绷旗。 火紅的嫁衣襯著肌膚如雪喜鼓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天衔肢,我揣著相機與錄音庄岖,去河邊找鬼。 笑死角骤,一個胖子當著我的面吹牛隅忿,可吹牛的內容都是我干的。 我是一名探鬼主播启搂,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼硼控,長吁一口氣:“原來是場噩夢啊……” “哼刘陶!你這毒婦竟也來了胳赌?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤匙隔,失蹤者是張志新(化名)和其女友劉穎疑苫,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纷责,經...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡捍掺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了再膳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挺勿。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖喂柒,靈堂內的尸體忽然破棺而出不瓶,到底是詐尸還是另有隱情禾嫉,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布蚊丐,位于F島的核電站熙参,受9級特大地震影響,放射性物質發(fā)生泄漏麦备。R本人自食惡果不足惜孽椰,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望凛篙。 院中可真熱鬧黍匾,春花似錦、人聲如沸呛梆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽削彬。三九已至全庸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間融痛,已是汗流浹背壶笼。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留雁刷,地道東北人覆劈。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像沛励,于是被迫代替她去往敵國和親责语。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內容