UITableView+FDTemplateLayoutCell 源碼閱讀

UITableViewCell 高度計(jì)算

UITableView 詢問 cell 高度有兩種方式
1.rowHeight屬性垛孔。所有Cell都為固定高度,這種情況下最好不要使用下面第2種方法先朦。
2.- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath代理方法,它會(huì)使rowHeight屬性的設(shè)置失效犬缨。

在自定義tableViewCell的時(shí)候,是否想過先在cellForRow...方法里設(shè)置了數(shù)據(jù)模型棉浸,然后獲得cell準(zhǔn)確高度怀薛,再在heightForRow...方法設(shè)置高度?但heightForRow...是比cellForRow...要先調(diào)用的迷郑,也就是調(diào)用heightForRow...時(shí)還不知道行高枝恋。

實(shí)際加載tableView的過程中發(fā)現(xiàn)创倔,tableView幾個(gè)代理方法調(diào)用順序如下
1.調(diào)用numberOfRow...等詢問有多少個(gè)cell
2.調(diào)用heightForRow...n次,n=cell的總個(gè)數(shù)
3.對(duì)當(dāng)前一屏顯示的x個(gè)cell焚碌,先調(diào)用cellForRow...繪制畦攘,再調(diào)用heightForRow...(依次交替調(diào)用x次)
4.當(dāng)屏幕滾動(dòng),有新的cell出現(xiàn)在屏幕上十电,同3知押,先調(diào)cellForRow...再調(diào)heightForRow...

tableView繼承自scrollView ,它需要知道自己的contentSize鹃骂。因此它在一開始加載的時(shí)候台盯,對(duì)每個(gè)cell使用代理方法獲得它的高度 方便得到contentSize,進(jìn)而得到滾動(dòng)進(jìn)度條的位置畏线。但是静盅,如果cell太多,那么在首次加載的時(shí)候寝殴,會(huì)引發(fā)性能問題蒿叠,浪費(fèi)了多余的計(jì)算在屏幕外邊的 cell 上。

iOS7以后出現(xiàn)了預(yù)估高度estimatedRowHeight
對(duì)應(yīng)有:tableView: estimatedHeightForRowAtIndexPath:
如果設(shè)置了估算高度蚣常,避免了一開始調(diào)用n次heightForRow導(dǎo)致的一些不必要的計(jì)算市咽,而是直接用預(yù)估高度*cell個(gè)數(shù)來計(jì)算contentSize。當(dāng)在繪制一個(gè)單元格時(shí)史隆,才去獲取它的準(zhǔn)確高度魂务。(步驟1、3泌射、4不變)

但是估算高度也有不足的地方:優(yōu)化UITableViewCell高度計(jì)算的那些事

1.設(shè)置估算高度后粘姜,contentSize.height 根據(jù)“cell估算值 x cell個(gè)數(shù)”計(jì)算,這就導(dǎo)致滾動(dòng)條的大小處于不穩(wěn)定的狀態(tài)熔酷,contentSize 會(huì)隨著滾動(dòng)從估算高度慢慢替換成真實(shí)高度孤紧,肉眼可見滾動(dòng)條突然變化甚至“跳躍”。
2.若是有設(shè)計(jì)不好的下拉刷新或上拉加載控件拒秘,或是 KVO 了 contentSize 或 contentOffset 屬性号显,有可能使表格滑動(dòng)時(shí)跳動(dòng)。
3.估算高度設(shè)計(jì)初衷是好的躺酒,讓加載速度更快押蚤,那憑啥要去侵害滑動(dòng)的流暢性呢,用戶可能對(duì)進(jìn)入頁面時(shí)多零點(diǎn)幾秒加載時(shí)間感覺不大羹应,但是滑動(dòng)時(shí)實(shí)時(shí)計(jì)算高度帶來的卡頓是明顯能體驗(yàn)到的揽碘,個(gè)人覺得還不如一開始都算好了呢(iOS8更過分,即使都算好了也會(huì)邊劃邊計(jì)算)

UITableView+FDTemplateLayoutCell

iOS8 之前雖然采用 autoLayout 相比 frame layout 得手動(dòng)計(jì)算已經(jīng)簡(jiǎn)化了不少:設(shè)置 estimatedRowHeight 屬性、對(duì)cell設(shè)置正確的約束雳刺、contentView 執(zhí)行 systemLayoutSizeFittingSize: 方法劫灶。但需要維護(hù)專門為計(jì)算高度而生的模板cell,以及UILabel 折行問題等掖桦。

iOS8后出現(xiàn)self-sizing cell本昏,設(shè)置好約束后,直接設(shè)置 estimatedRowHeight 就可以了枪汪。但是cell高度沒有緩存機(jī)制涌穆,不論何時(shí)都會(huì)重新計(jì)算 cell 高度。這樣就會(huì)導(dǎo)致滑動(dòng)不流暢料饥。

優(yōu)化的方式:對(duì)于已經(jīng)計(jì)算了高度的 Cell蒲犬,就將這個(gè)高度緩存起來,下次調(diào)用heightForRow...方法時(shí)岸啡,返回高度緩存就行了原叮。UITableView+FDTemplateLayoutCell這個(gè)第三方開源主要做的就是這個(gè)事。

高度緩存

1.FDIndexPathHeightCache緩存策略

  • 創(chuàng)建了一個(gè)類FDIndexPathHeightCache來進(jìn)行高度緩存的創(chuàng)建巡蘸、存取奋隶。

針對(duì)橫屏\豎屏分別聲明了 2 個(gè)以 indexPath 為索引的二維數(shù)組來存儲(chǔ)高度(section、row - 二維)悦荒。第一維定位到 Section唯欣,后一維定位到 Row,這樣就可以同時(shí)管到 Sections 和 Rows 的數(shù)據(jù)變動(dòng)搬味。

typedef NSMutableArray<NSMutableArray<NSNumber *> *> FDIndexPathHeightsBySection;

@interface FDIndexPathHeightCache ()
@property (nonatomic, strong) FDIndexPathHeightsBySection *heightsBySectionForPortrait;//豎屏?xí)r的基于indexPath高度緩存
@property (nonatomic, strong) FDIndexPathHeightsBySection *heightsBySectionForLandscape;//橫屏?xí)r的基于indexPath高度緩存
@end

使用indexPath 作為索引境氢,在發(fā)生刪除or插入單元格之后,緩存中的索引就需要進(jìn)行相應(yīng)的變動(dòng)碰纬,使用NSMutableArray能很方便適應(yīng)這種變動(dòng)萍聊。

如何創(chuàng)建高度緩存、分配空間悦析、初始化高度為-1寿桨,以及賦高度值到緩存數(shù)組中儲(chǔ)存和從緩存中取高度值,這些閱讀源碼都可以很好理解强戴,這里不多說亭螟。

  • 分類 UITableView (FDIndexPathHeightCache)
 @implementation UITableView (FDIndexPathHeightCache)
//懶加載?高度緩存
 - (FDIndexPathHeightCache *)fd_indexPathHeightCache {
    FDIndexPathHeightCache *cache = objc_getAssociatedObject(self, _cmd);
    if (!cache) {
        [self methodSignatureForSelector:nil];
        cache = [FDIndexPathHeightCache new];//執(zhí)行init方法骑歹,初始化了兩個(gè)橫屏预烙、豎屏?xí)r的高度數(shù)組
        objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return cache;
}
@end

在UITableView+FDTemplateLayoutCell 框架中多處使用了runtime 的關(guān)聯(lián)對(duì)象Associated Object來進(jìn)行給類添加公有和私有變量。
_cmd表示當(dāng)前方法的Selector道媚。

OC 中可以通過 Category 給一個(gè)現(xiàn)有的類添加屬性扁掸,但是卻不能添加實(shí)例變量(即下劃線變量欢嘿,不過一般說法是不能添加屬性),這個(gè)“缺點(diǎn)”可以通過 Associated Objects 來彌補(bǔ)也糊。Associated Objects的使用樣例:1. 添加私有屬性用于更好地去實(shí)現(xiàn)細(xì)節(jié)。2.添加public屬性來增強(qiáng)category的功能羡宙。3.創(chuàng)建一個(gè)用于KVO的關(guān)聯(lián)觀察者狸剃。
關(guān)聯(lián)是可以保證被關(guān)聯(lián)的對(duì)象在關(guān)聯(lián)對(duì)象的整個(gè)生命周期都是可用的。

關(guān)聯(lián)對(duì)象 在這里的作用就相當(dāng)于是懶加載狗热,就是在用到相關(guān)的緩存策略時(shí)才會(huì)初始化(這里就初始化了橫豎屏?xí)r的兩個(gè)二維數(shù)組)钞馁。另外,它將內(nèi)存的釋放托管給了 UITableView 實(shí)例的生命周期匿刮,不用管釋放內(nèi)存的事情了僧凰。
Objective-C Associated Objects 的實(shí)現(xiàn)原理

ps:[self methodSignatureForSelector:nil];這句runtime的沒太懂什么作用。

  • 分類 UITableView (FDIndexPathHeightCacheInvalidation)
// We just forward primary call, in crash report, top most method in stack maybe FD's,
// but it's really not our bug, you should check whether your table view's data source and
// displaying cells are not matched when reloading.
static void __FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(void (^callout)(void)) {
    callout();
}
#define FDPrimaryCall(...) do {__FD_TEMPLATE_LAYOUT_CELL_PRIMARY_CALL_IF_CRASH_NOT_OUR_BUG__(^{__VA_ARGS__});} while(0)//宏定義.__VA_ARGS_ 就是直接將括號(hào)里的...轉(zhuǎn)化為實(shí)際的字符

調(diào)試時(shí)用的熟丸?看調(diào)用棧训措?沒看太懂 。注釋:“在崩潰報(bào)告中光羞,調(diào)用棧頂?shù)姆椒赡苁荈D的方法绩鸣,要檢查一下當(dāng)reload時(shí)dataSource和正在顯示的cell是否不對(duì)應(yīng)∩炊遥”

更新處理

+ (void)load {
   // All methods that trigger height cache's invalidation  9個(gè)方法
   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 {//重寫的reload方法呀闻,替換tableView里的reload方法
    if (self.fd_indexPathHeightCache.automaticallyInvalidateEnabled) {
        [self.fd_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(FDIndexPathHeightsBySection *heightsBySection) {
            [heightsBySection removeAllObjects];
        }];
    }
    FDPrimaryCall([self fd_reloadData];);//不是遞歸調(diào)用。是調(diào)用原來的方法潜慎?
}

IndexPathHeightCache 在實(shí)現(xiàn)上需要在插入捡多、刪除cell變動(dòng)時(shí)更新高度緩存。
有種做法是:子類化uitableview,重寫相關(guān)方法铐炫,然后使用這些子類垒手。FDIndexPathHeightCache重寫了UITableView的9個(gè)觸發(fā)刷新的相關(guān)方法,并利用 runtime 的method_exchangeImplementations函數(shù)對(duì)這9個(gè)方法做了替換驳遵,對(duì)高度緩存進(jìn)行更新淫奔。這種做法更加簡(jiǎn)單靈活。

這里在+load方法里堤结,利用 Runtime 特性把一個(gè)方法的實(shí)現(xiàn)與另一個(gè)方法的實(shí)現(xiàn)進(jìn)行替換唆迁,實(shí)現(xiàn)Method Swizzling 。
Objective C類方法load和initialize的區(qū)別
Method Swizzling 和 AOP 實(shí)踐
Objective-C Method Swizzling 的最佳實(shí)踐

//用于需要刷新數(shù)據(jù)但不想移除原有緩存數(shù)據(jù)(框架內(nèi)對(duì) reloadData 方法的處理是清空緩存)時(shí)調(diào)用竞穷,比如常見的“下拉加載更多數(shù)據(jù)”操作唐责。
- (void)fd_reloadDataWithoutInvalidateIndexPathHeightCache {
   FDPrimaryCall([self fd_reloadData];);
}

用于需要刷新數(shù)據(jù)但不想移除原有緩存數(shù)據(jù)(框架內(nèi)對(duì) reloadData 方法的處理是清空緩存)時(shí)調(diào)用,比如常見的“下拉加載更多數(shù)據(jù)”操作瘾带。

2.FDKeyedHeightCache緩存策略

除了提供了indexPath作為索引的方式鼠哥,還提供了另外一個(gè) API:把數(shù)據(jù)模型的唯一標(biāo)識(shí)key用作索引
- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id<NSCopying>)key configuration:(void (^)(id cell))configuration;

FDKeyedHeightCache采用字典做緩存,沒有復(fù)雜的數(shù)組構(gòu)建、存取操作朴恳,源碼實(shí)現(xiàn)上相比于FDIndexPathHeightCache要簡(jiǎn)單得多抄罕。當(dāng)然,在刪除于颖、插入呆贿、刷新 相關(guān)的緩存操作并沒有實(shí)現(xiàn),因此需要開發(fā)者來自己完成森渐。

一般來說 cacheByIndexPath: 方法最為“傻瓜”做入,可以直接搞定所用問題。cacheByKey: 方法稍顯復(fù)雜(需要關(guān)注數(shù)據(jù)刷新)同衣,但在緩存機(jī)制上相比 cacheByIndexPath: 方法更為高效竟块。因此,像類似微博耐齐、新聞這種會(huì)擁有唯一標(biāo)識(shí)的 cell 數(shù)據(jù)模型浪秘,更建議使用cacheByKey: 方法。

如果cell高度發(fā)生變化(數(shù)據(jù)源改變)蚪缀,那么需要手動(dòng)對(duì)高度緩存進(jìn)行處理:

- (void)invalidateHeightForKey:(id<NSCopying>)key {
    [self.mutableHeightsByKeyForPortrait removeObjectForKey:key];
    [self.mutableHeightsByKeyForLandscape removeObjectForKey:key];
}

- (void)invalidateAllHeightCache {
    [self.mutableHeightsByKeyForPortrait removeAllObjects];
    [self.mutableHeightsByKeyForLandscape removeAllObjects];
}

高度獲取

  • 獲取高度的過程:以indexPath為例秫逝,key的實(shí)現(xiàn)大致相同。
 //FDSimulatedCacheModeCacheByIndexPath模式询枚。建立基于indexpath的高度緩存數(shù)組(空間)违帆,返回高度
 - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration {
    if (!identifier || !indexPath) {
        return 0;
    }
    
    // Hit cache 已經(jīng)建立了高度緩存,命中緩存
    if ([self.fd_indexPathHeightCache existsHeightAtIndexPath:indexPath]) {
        //debug打印
        [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];
    }
    //還沒建立高度緩存金蜀。調(diào)用fd_heightForCellWithIdentifier: configuration: 方法計(jì)算獲得 cell 高度
    CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];//創(chuàng)建templateCell刷后,計(jì)算高度
    [self.fd_indexPathHeightCache cacheHeight:height byIndexPath:indexPath];//插入緩存
    [self fd_debugLog:[NSString stringWithFormat: @"cached by index path[%@:%@] - %@", @(indexPath.section), @(indexPath.row), @(height)]];
    
    return height;
}

這里- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id cell))configuration方法對(duì)應(yīng)FDSimulatedCacheModeNone模式(沒有建立緩存)。用于創(chuàng)建渊抄、配置一個(gè)和tableview cell 布局相同的TemplateCell(模板cell)尝胆,并計(jì)算它的高度。

  • 創(chuàng)建模板cell
//返回一個(gè)template Cell
 - (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier {
    NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
    //儲(chǔ)存單元格的字典护桦。一種identifier對(duì)應(yīng)一個(gè)templateCell
    NSMutableDictionary<NSString *, UITableViewCell *> *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;//runtime關(guān)聯(lián)含衔。不過這個(gè)屬性的get方法似乎沒有被調(diào)用。使用 UITableViewCell 模板Cell計(jì)算高度二庵,通過 fd_isTemplateLayoutCell 可在Cell內(nèi)部判斷當(dāng)前是否是模板Cell贪染。可以省去一些與高度無關(guān)的操作催享。
        templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO;
        templateCellsByIdentifiers[identifier] = templateCell;
        [self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]];
    }
    
    return templateCell;
}

fd_isTemplateLayoutCell屬性:模板cell僅用來計(jì)算高度杭隙,通過 fd_isTemplateLayoutCell 可在Cell內(nèi)部判斷當(dāng)前是否是模板Cell。若是模板cell可以省去一些與高度計(jì)算無關(guān)的操作因妙。


  • templateCell高度計(jì)算
    - (CGFloat)fd_systemFittingHeightForConfiguratedCell:(UITableViewCell *)cell中有段注釋說明算高的流程:
    // 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

默認(rèn)情況下是使用autoLayout的(fd_enforceFrameLayout屬性默認(rèn)為NO)痰憎,如果使用的是frameLayout則設(shè)置fd_enforceFrameLayout為YES票髓,代碼會(huì)根據(jù)你使用的layout模式來計(jì)算template Cell的高度。使用autoLayout的用systemLayoutSizeFittingSize:方法铣耘。使用frameLayout需要在自定義Cell里重寫sizeThatFit:方法洽沟。如果兩種模式都沒有使用,單元格高度設(shè)為默認(rèn)的44蜗细。
fd_enforceFrameLayout屬性不需要手動(dòng)設(shè)置:it will automatically choose a proper mode by whether you have set auto layout constrants on cell's content view.

關(guān)于UILable的問題

當(dāng) UILabel 行數(shù)大于0時(shí)玲躯,需要指定 preferredMaxLayoutWidth 后它才知道自己什么時(shí)候該折行。這是個(gè)“雞生蛋蛋生雞”的問題鳄乏,因?yàn)?UILabel 需要知道 superview 的寬度才能折行,而 superview 的寬度還依仗著子 view 寬度的累加才能確定棘利。

框架中的做法是:先計(jì)算contentView的寬度橱野,然后對(duì)contentView添加寬度約束,然后使用systemLayoutSizeFittingSize:計(jì)算獲得高度善玫,計(jì)算完成以后移除contentView的寬度約束水援。

CGFloat contentViewWidth = CGRectGetWidth(self.frame);//先設(shè)置contentView的寬度等于tableView的寬度
    
    // If a cell has accessory view or system accessory type, its content view's width is smaller
    // than cell's by some fixed values.
    //如果單元格有accessory類型或者accessory子視圖的,contentView的寬度要減去這一部分
    if (cell.accessoryView) {        contentViewWidth -= 16 + CGRectGetWidth(cell.accessoryView.frame);
    } else {
        static const CGFloat systemAccessoryWidths[] = {
            [UITableViewCellAccessoryNone] = 0,
            [UITableViewCellAccessoryDisclosureIndicator] = 34,
            [UITableViewCellAccessoryDetailDisclosureButton] = 68,
            [UITableViewCellAccessoryCheckmark] = 40,
            [UITableViewCellAccessoryDetailButton] = 48
        };
        contentViewWidth -= systemAccessoryWidths[cell.accessoryType];
    }

    CGFloat fittingHeight = 0;
    
    if (!cell.fd_enforceFrameLayout && contentViewWidth > 0) {//不使用frameLayout
        // Add a hard width constraint to make dynamic content views (like labels) expand vertically instead
        // of growing horizontally, in a flow-layout manner.
        NSLayoutConstraint *widthFenceConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:contentViewWidth];//寬度約束
        [cell.contentView addConstraint:widthFenceConstraint];
        
        // Auto layout engine does its math
        fittingHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;//算高
        [cell.contentView removeConstraint:widthFenceConstraint];//移除寬度約束
        
        [self fd_debugLog:[NSString stringWithFormat:@"calculate using system fitting size (AutoLayout) - %@", @(fittingHeight)]];
    }

如果使用的是frameLayout茅郎,重寫sizeThatFits:并用數(shù)據(jù)內(nèi)容來反算高度蜗元。
fittingHeight = [cell sizeThatFits:CGSizeMake(contentViewWidth, 0)].height;

 - (CGSize)sizeThatFits:(CGSize)size {
    CGFloat totalHeight = 0;
    totalHeight += [self.titleLabel sizeThatFits:size].height;
    totalHeight += [self.contentLabel sizeThatFits:size].height;
    totalHeight += [self.contentImageView sizeThatFits:size].height;
    totalHeight += [self.usernameLabel sizeThatFits:size].height;
    totalHeight += 40; // margins
    return CGSizeMake(size.width, totalHeight);
}

最后視情況而定是否需要加上分割線高度:

     if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
        fittingHeight += 1.0 / [UIScreen mainScreen].scale;
    }

其他

__kindof :一般用在方法的返回值,返回類或者其子類都是合法的系冗。http://www.reibang.com/p/3f73e696dd4d

使用

注意的地方:
1.使用storyboard創(chuàng)建cell奕扣,要保證 contentView 內(nèi)部上下左右所有方向都有約束支撐。

2.使用代碼或 XIB 創(chuàng)建的 cell掌敬,使用以下注冊(cè)方法:

- (void)registerClass:(nullableClass)cellClassforCellReuseIdentifier:(NSString *)identifier;
- (void)registerNib:(nullableUINib *)nibforCellReuseIdentifier:(NSString *)identifier;

3.cell通過-dequeueCellForReuseIdentifier:來創(chuàng)建惯豆。

4.在-tableView:heightForRowAtIndexPath:方法中調(diào)用cacheByIndexPath或者cacheByKey的方法完成高度緩存的創(chuàng)建和獲取。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [tableView fd_heightForCellWithIdentifier:@"identifer" cacheByIndexPath:indexPath configuration:^(id cell) {
        // configurations
    }];
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    Entity *entity = self.entities[indexPath.row];
    return [tableView fd_heightForCellWithIdentifier:@"identifer" cacheByKey:entity.uid configuration:^(id cell) {
        // configurations
    }];
}

5.不需要再設(shè)置estimatedRowHeight屬性

這里以使用autolayout的情況為例奔害,使用frameLayout的情況不作說明楷兽,以下demo數(shù)據(jù)來自原作demo。

  • 使用storyboard
    UITableView+FDTemplateLayoutCell框架中的demo就是使用storyboard實(shí)現(xiàn)的华临,非常簡(jiǎn)單易懂不作過多說明芯杀。下面說一些要注意的地方:
    在子線程解析json數(shù)據(jù)然后再回到主線程刷新tableView。以前自己一般的做法是設(shè)置一個(gè)NSMutableArray類型屬性用來儲(chǔ)存模型數(shù)據(jù)雅潭,然后在懶加載中解析數(shù)據(jù)揭厚。


imageview 的mode設(shè)置為aspect fit,在保持長(zhǎng)寬比的前提下寻馏,縮放圖片棋弥,使得圖片在容器內(nèi)完整顯示出來。


imageView注意1.png

然后imageView的右約束是一個(gè)不等于約束诚欠,intrinsic size 設(shè)為placeholder顽染。這是因?yàn)椋喝绻麅?nèi)容是運(yùn)行時(shí)決定的如UIImageView漾岳,若圖片是從服務(wù)器下載的,那么我們就需要放一個(gè)空的UIImageView粉寞,不包含所顯示的圖片尼荆,不過這樣會(huì)因未設(shè)置圖片導(dǎo)致imageView尺寸無法確定,storyboard拋出錯(cuò)誤唧垦,解決方案便是放一個(gè)臨時(shí)的占位尺寸來告訴sotryboard捅儒。


imageView注意2.png
  • 使用純代碼,autolayout
    參照storyboard約束設(shè)置用純代碼寫約束條件振亮。
    自定義cell里面的實(shí)現(xiàn):初始化的方法內(nèi)部創(chuàng)建子控件并且使用Masonry布局



    initSubview方法的實(shí)現(xiàn)巧还,保證 contentView 內(nèi)部上下左右所有方向都有約束支撐:



    ps:這樣子設(shè)置約束還是會(huì)有點(diǎn)問題(包括原作的例子),想想如果標(biāo)題坊秸、正文內(nèi)容麸祷、圖片或者名字其中一個(gè)子控件賦值為空,但是約束仍然存在褒搔,這種情況下應(yīng)該怎樣處理阶牍。
    更新:解決辦法。
 #import "HXTableViewCell.h"
 #import "Masonry.h"
 @interface HXTableViewCell()
 @property (weak, nonatomic) UILabel *title;
 @property (weak, nonatomic) UILabel *content;
 @property (weak, nonatomic) UILabel *name;
 @property (weak, nonatomic) UILabel *time;
 @property (weak, nonatomic) UIImageView *image;
  
 @property (nonatomic,strong) MASConstraint *contentConstraint;
 @property (nonatomic,strong) MASConstraint *imgConstraint;
 @property (nonatomic,strong) MASConstraint *titleConstraint;
 @end
 
 @implementation HXTableViewCell
 - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        [self initSubView];
    }
    return self;
}
 
 - (void)setDatamodel:(DataModel *)datamodel{
    _datamodel = datamodel;
    self.title.text = datamodel.title;
    self.content.text = datamodel.content;
    self.name.text = datamodel.username;
    self.time.text = datamodel.time;
    self.image.image = datamodel.imageName.length > 0 ? [UIImage imageNamed:datamodel.imageName] : nil;
 
    self.title.text.length ==  0 ? [self.titleConstraint deactivate]:[self.titleConstraint activate];
    self.content.text.length ==  0 ? [self.contentConstraint deactivate]:[self.contentConstraint activate];
    self.image.image == nil ? [self.imgConstraint deactivate]:[self.imgConstraint activate];
}

 - (void)initSubView{
    UILabel *title = [[UILabel alloc]init];
    _title = title;
    _title.numberOfLines = 0;//多行文字
    [self.contentView addSubview:_title];
     
    UILabel *content = [[UILabel alloc]init];
    _content = content;
    _content.numberOfLines = 0;//多行文字
    [self.contentView addSubview:_content];
     
    UILabel *name = [[UILabel alloc]init];
    _name = name;
    _name.font = [UIFont systemFontOfSize:14.0];
    [self.contentView addSubview:_name];
     
    UILabel *time = [[UILabel alloc]init];
    _time = time;
    _time.font = [UIFont systemFontOfSize:14.0];
    [self.contentView addSubview:_time];
     
    UIImageView *image = [[UIImageView alloc]init];
    _image = image;
    _image.contentMode = UIViewContentModeScaleAspectFill;
    [self.contentView addSubview:_image];
     
    int padding = 20;
    __weak typeof(self) weakself = self;
    [_title mas_makeConstraints:^(MASConstraintMaker *make) {
        //以下設(shè)置距離contentView的邊距,設(shè)置兩條優(yōu)先度不同的約束星瘾,內(nèi)容為空時(shí)將優(yōu)先度高的約束禁用
        make.top.equalTo(weakself.contentView).priorityLow();
        weakself.titleConstraint = make.top.mas_equalTo(weakself.contentView).offset(20).priorityHigh();
         
        make.left.mas_equalTo(weakself.contentView).offset(padding);
        make.right.mas_equalTo(weakself.contentView.mas_right).offset(-padding);
    }];
    [_content mas_makeConstraints:^(MASConstraintMaker *make) {
        //以下設(shè)置距離title的邊距,設(shè)置兩條優(yōu)先度不同的約束走孽,內(nèi)容為空時(shí)將優(yōu)先度高的約束禁用
        make.top.equalTo(_title.mas_bottom).priorityLow();
        weakself.contentConstraint = make.top.mas_equalTo(_title.mas_bottom).offset(20).priorityHigh();
         
        make.leading.mas_equalTo(_title.mas_leading);
        make.right.mas_equalTo(weakself.contentView.mas_right).offset(-padding);
    }];
     
    [_image mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(_content.mas_bottom).priorityLow();
        weakself.imgConstraint = make.top.mas_equalTo(weakself.content.mas_bottom).offset(20).priorityHigh();
         
        make.leading.mas_equalTo(_title.mas_leading);
    }];
    [_name mas_makeConstraints:^(MASConstraintMaker *make) {
        make.leading.mas_equalTo(_title.mas_leading);
        make.top.mas_equalTo(_image.mas_bottom).offset(20);
         make.bottom.mas_equalTo(weakself.contentView.mas_bottom).offset(-10);
    }];
    [_time mas_makeConstraints:^(MASConstraintMaker *make) {
        make.right.mas_equalTo(weakself.contentView.mas_right).offset(-padding);
        make.baseline.mas_equalTo(_name.mas_baseline);
    }];
}
@end

控制器中的實(shí)現(xiàn):基本和原作demo中的差不多,一定要使用- registerClass:forCellReuseIdentifier:方法注冊(cè)琳状。而且應(yīng)該像原作demo中在子線程解析json數(shù)據(jù)然后再回到主線程刷新tableView

有個(gè)奇怪的現(xiàn)象:如果vc中的數(shù)據(jù)模型是二維數(shù)組(section \row)的話只會(huì)計(jì)算磕瓷、緩存一次高度。如果是一維數(shù)組念逞,就會(huì)計(jì)算生宛、緩存兩次高度(重復(fù)兩次)。不知道為什么肮柜。代碼如下:

 - (void)viewDidLoad {
    [super viewDidLoad];

    self.tableView.fd_debugLogEnabled = YES;
    [self buildTestDataThen:^{
        [self.tableView reloadData];
    }];
}

 - (void)buildTestDataThen:(void (^)(void))then{
    // Simulate an async request
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        // Data from `data.json`
        NSString *dataFilePath = [[NSBundle mainBundle] pathForResource:@"data" ofType:@"json"];
        NSData *data = [NSData dataWithContentsOfFile:dataFilePath];
        NSDictionary *rootDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
        NSArray *feedDicts = rootDict[@"feed"];
        
        // Convert to `FDFeedEntity`
        NSMutableArray *entities = @[].mutableCopy;
        [feedDicts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            [entities addObject:[[DataModel alloc] initWithDictionary:obj]];
        }];
        self.cellData = entities;
        
        // Callback
        dispatch_async(dispatch_get_main_queue(), ^{
            !then ?: then();
        });
    });
}

 - (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
 
 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}
 
 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.cellData count];
}
 
 - (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    HXTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"FDDemo"];
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}
 
  - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return [tableView fd_heightForCellWithIdentifier:@"FDDemo" cacheByIndexPath:indexPath configuration:^(HXTableViewCell *cell) {
        [self configureCell:cell atIndexPath:indexPath];
    }];
}
 
 - (void)configureCell:(HXTableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    cell.fd_enforceFrameLayout = NO; 
    cell.datamodel = self.cellData[indexPath.row];
}

參考文章:
UITableViewCell 自動(dòng)高度
優(yōu)化UITableViewCell高度計(jì)算的那些事
UITableView+FDTemplateLayoutCell 框架學(xué)習(xí)
UITableView-FDTemplateLayoutCell源碼分析
有了Auto Layout,為什么你還是害怕寫UITabelView的自適應(yīng)布局?
使用Autolayout實(shí)現(xiàn)UITableView的Cell動(dòng)態(tài)布局和高度動(dòng)態(tài)改變

更新:
關(guān)于UITableView+FDTemplateLayoutCell的1.2版本中利用RunLoop空閑時(shí)間執(zhí)行預(yù)緩存任務(wù)(雖然預(yù)緩存功能因?yàn)橄吕⑿碌臎_突和不明顯的收益已經(jīng)廢棄)

sunny博客原文在這一部分已經(jīng)講述得比較清楚了陷舅,這里總結(jié)一下
先來看看runloop內(nèi)部邏輯:


RunLoop 內(nèi)部的邏輯

預(yù)緩存高度 要求頁面處于空閑狀態(tài)時(shí)才執(zhí)行高度計(jì)算,當(dāng)用戶正在滑動(dòng)列表時(shí)不應(yīng)該執(zhí)行計(jì)算任務(wù)影響滑動(dòng)體驗(yàn)审洞,需要在最無感知的時(shí)刻進(jìn)行莱睁,所以應(yīng)該同時(shí)滿足:
1.RunLoop 處于“空閑”狀態(tài)(defaultMode)
2。當(dāng)這一次 RunLoop 迭代處理完成了所有事件芒澜,馬上要休眠時(shí)

注冊(cè) RunLoopObserver 可以觀測(cè)當(dāng)前 RunLoop 的運(yùn)行狀態(tài)仰剿,每個(gè) Observer 都包含了一個(gè)回調(diào)(函數(shù)指針),當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí)痴晦,觀察者就能通過回調(diào)接受到這個(gè)變化南吮。可以觀測(cè)的時(shí)間點(diǎn)有以下幾個(gè):

在源代碼中對(duì)應(yīng)的就是:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即將進(jìn)入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 剛從休眠中喚醒
    kCFRunLoopExit          = (1UL << 7), // 即將退出Loop
};

FD框架里面做的主要兩個(gè)事情:
1.創(chuàng)建observer觀測(cè)runloop即將進(jìn)入休眠(kCFRunLoopBeforeWaiting)誊酌,
2.在observer的回調(diào)里收集部凑、分發(fā)任務(wù)(分發(fā)到多個(gè)runloop中執(zhí)行避免卡主線程)露乏。
利用performSelector這個(gè)api創(chuàng)建一個(gè) Source 0 任務(wù),分發(fā)到指定線程的 RunLoop 中涂邀,在給定的 Mode 下執(zhí)行瘟仿,若指定的 RunLoop 處于休眠狀態(tài),則喚醒它處理事件(上面圖中第七步比勉,source0任務(wù)可以喚醒runloop)

- (void)performSelector:(SEL)aSelector
               onThread:(NSThread *)thr
             withObject:(id)arg
          waitUntilDone:(BOOL)wait
                  modes:(NSArray *)array;

參考:
深入理解RunLoop
Cocoa深入學(xué)習(xí):NSOperationQueue劳较、NSRunLoop和線程安全

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市浩聋,隨后出現(xiàn)的幾起案子观蜗,更是在濱河造成了極大的恐慌,老刑警劉巖衣洁,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嫂便,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡闸与,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門岸售,熙熙樓的掌柜王于貴愁眉苦臉地迎上來践樱,“玉大人,你說我怎么就攤上這事凸丸】叫希” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵屎慢,是天一觀的道長(zhǎng)瞭稼。 經(jīng)常有香客問我,道長(zhǎng)腻惠,這世上最難降的妖魔是什么环肘? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮集灌,結(jié)果婚禮上悔雹,老公的妹妹穿的比我還像新娘。我一直安慰自己欣喧,他們只是感情好腌零,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著唆阿,像睡著了一般益涧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上驯鳖,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天闲询,我揣著相機(jī)與錄音久免,去河邊找鬼。 笑死嘹裂,一個(gè)胖子當(dāng)著我的面吹牛妄壶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播寄狼,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼丁寄,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了泊愧?” 一聲冷哼從身側(cè)響起伊磺,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎删咱,沒想到半個(gè)月后屑埋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡痰滋,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年摘能,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片敲街。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡团搞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出多艇,到底是詐尸還是另有隱情逻恐,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布峻黍,位于F島的核電站复隆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏姆涩。R本人自食惡果不足惜挽拂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望骨饿。 院中可真熱鬧轻局,春花似錦、人聲如沸样刷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽置鼻。三九已至镇饮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間箕母,已是汗流浹背储藐。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工俱济, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人钙勃。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓蛛碌,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親辖源。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蔚携,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

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