UITableView+FDTemplateLayoutCell 源碼閱讀

UITableViewCell 高度計算

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

在自定義tableViewCell的時候,是否想過先在cellForRow...方法里設置了數據模型,然后獲得cell準確高度,再在heightForRow...方法設置高度?但heightForRow...是比cellForRow...要先調用的,也就是調用heightForRow...時還不知道行高。

實際加載tableView的過程中發現,tableView幾個代理方法調用順序如下
1.調用numberOfRow...等詢問有多少個cell
2.調用heightForRow...n次,n=cell的總個數
3.對當前一屏顯示的x個cell,先調用cellForRow...繪制,再調用heightForRow...(依次交替調用x次)
4.當屏幕滾動,有新的cell出現在屏幕上,同3,先調cellForRow...再調heightForRow...

tableView繼承自scrollView ,它需要知道自己的contentSize。因此它在一開始加載的時候,對每個cell使用代理方法獲得它的高度 方便得到contentSize,進而得到滾動進度條的位置。但是,如果cell太多,那么在首次加載的時候,會引發性能問題,浪費了多余的計算在屏幕外邊的 cell 上。

iOS7以后出現了預估高度estimatedRowHeight
對應有:tableView: estimatedHeightForRowAtIndexPath:
如果設置了估算高度,避免了一開始調用n次heightForRow導致的一些不必要的計算,而是直接用預估高度*cell個數來計算contentSize。當在繪制一個單元格時,才去獲取它的準確高度。(步驟1、3、4不變)

但是估算高度也有不足的地方:優化UITableViewCell高度計算的那些事

1.設置估算高度后,contentSize.height 根據“cell估算值 x cell個數”計算,這就導致滾動條的大小處于不穩定的狀態,contentSize 會隨著滾動從估算高度慢慢替換成真實高度,肉眼可見滾動條突然變化甚至“跳躍”。
2.若是有設計不好的下拉刷新或上拉加載控件,或是 KVO 了 contentSize 或 contentOffset 屬性,有可能使表格滑動時跳動。
3.估算高度設計初衷是好的,讓加載速度更快,那憑啥要去侵害滑動的流暢性呢,用戶可能對進入頁面時多零點幾秒加載時間感覺不大,但是滑動時實時計算高度帶來的卡頓是明顯能體驗到的,個人覺得還不如一開始都算好了呢(iOS8更過分,即使都算好了也會邊劃邊計算)

UITableView+FDTemplateLayoutCell

iOS8 之前雖然采用 autoLayout 相比 frame layout 得手動計算已經簡化了不少:設置 estimatedRowHeight 屬性、對cell設置正確的約束、contentView 執行 systemLayoutSizeFittingSize: 方法。但需要維護專門為計算高度而生的模板cell,以及UILabel 折行問題等。

iOS8后出現self-sizing cell,設置好約束后,直接設置 estimatedRowHeight 就可以了。但是cell高度沒有緩存機制,不論何時都會重新計算 cell 高度。這樣就會導致滑動不流暢。

優化的方式:對于已經計算了高度的 Cell,就將這個高度緩存起來,下次調用heightForRow...方法時,返回高度緩存就行了。UITableView+FDTemplateLayoutCell這個第三方開源主要做的就是這個事。

高度緩存

1.FDIndexPathHeightCache緩存策略

  • 創建了一個類FDIndexPathHeightCache來進行高度緩存的創建、存取。

針對橫屏\豎屏分別聲明了 2 個以 indexPath 為索引的二維數組來存儲高度(section、row - 二維)。第一維定位到 Section,后一維定位到 Row,這樣就可以同時管到 Sections 和 Rows 的數據變動。

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

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

使用indexPath 作為索引,在發生刪除or插入單元格之后,緩存中的索引就需要進行相應的變動,使用NSMutableArray能很方便適應這種變動。

如何創建高度緩存、分配空間、初始化高度為-1,以及賦高度值到緩存數組中儲存和從緩存中取高度值,這些閱讀源碼都可以很好理解,這里不多說。

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

在UITableView+FDTemplateLayoutCell 框架中多處使用了runtime 的關聯對象Associated Object來進行給類添加公有和私有變量。
_cmd表示當前方法的Selector。

OC 中可以通過 Category 給一個現有的類添加屬性,但是卻不能添加實例變量(即下劃線變量,不過一般說法是不能添加屬性),這個“缺點”可以通過 Associated Objects 來彌補。Associated Objects的使用樣例:1. 添加私有屬性用于更好地去實現細節。2.添加public屬性來增強category的功能。3.創建一個用于KVO的關聯觀察者。
關聯是可以保證被關聯的對象在關聯對象的整個生命周期都是可用的。

關聯對象 在這里的作用就相當于是懶加載,就是在用到相關的緩存策略時才會初始化(這里就初始化了橫豎屏時的兩個二維數組)。另外,它將內存的釋放托管給了 UITableView 實例的生命周期,不用管釋放內存的事情了。
Objective-C Associated Objects 的實現原理

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_ 就是直接將括號里的...轉化為實際的字符

調試時用的?看調用棧?沒看太懂 。注釋:“在崩潰報告中,調用棧頂的方法可能是FD的方法,要檢查一下當reload時dataSource和正在顯示的cell是否不對應?!?/p>

更新處理

+ (void)load {
   // All methods that trigger height cache's invalidation  9個方法
   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];);//不是遞歸調用。是調用原來的方法?
}

IndexPathHeightCache 在實現上需要在插入、刪除cell變動時更新高度緩存。
有種做法是:子類化uitableview,重寫相關方法,然后使用這些子類。FDIndexPathHeightCache重寫了UITableView的9個觸發刷新的相關方法,并利用 runtime 的method_exchangeImplementations函數對這9個方法做了替換,對高度緩存進行更新。這種做法更加簡單靈活。

這里在+load方法里,利用 Runtime 特性把一個方法的實現與另一個方法的實現進行替換,實現Method Swizzling 。
Objective C類方法load和initialize的區別
Method Swizzling 和 AOP 實踐
Objective-C Method Swizzling 的最佳實踐

//用于需要刷新數據但不想移除原有緩存數據(框架內對 reloadData 方法的處理是清空緩存)時調用,比如常見的“下拉加載更多數據”操作。
- (void)fd_reloadDataWithoutInvalidateIndexPathHeightCache {
   FDPrimaryCall([self fd_reloadData];);
}

用于需要刷新數據但不想移除原有緩存數據(框架內對 reloadData 方法的處理是清空緩存)時調用,比如常見的“下拉加載更多數據”操作。

2.FDKeyedHeightCache緩存策略

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

FDKeyedHeightCache采用字典做緩存,沒有復雜的數組構建、存取操作,源碼實現上相比于FDIndexPathHeightCache要簡單得多。當然,在刪除、插入、刷新 相關的緩存操作并沒有實現,因此需要開發者來自己完成。

一般來說 cacheByIndexPath: 方法最為“傻瓜”,可以直接搞定所用問題。cacheByKey: 方法稍顯復雜(需要關注數據刷新),但在緩存機制上相比 cacheByIndexPath: 方法更為高效。因此,像類似微博、新聞這種會擁有唯一標識的 cell 數據模型,更建議使用cacheByKey: 方法。

如果cell高度發生變化(數據源改變),那么需要手動對高度緩存進行處理:

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

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

高度獲取

  • 獲取高度的過程:以indexPath為例,key的實現大致相同。
 //FDSimulatedCacheModeCacheByIndexPath模式。建立基于indexpath的高度緩存數組(空間),返回高度
 - (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration {
    if (!identifier || !indexPath) {
        return 0;
    }
    
    // Hit cache 已經建立了高度緩存,命中緩存
    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];
    }
    //還沒建立高度緩存。調用fd_heightForCellWithIdentifier: configuration: 方法計算獲得 cell 高度
    CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];//創建templateCell,計算高度
    [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方法對應FDSimulatedCacheModeNone模式(沒有建立緩存)。用于創建、配置一個和tableview cell 布局相同的TemplateCell(模板cell),并計算它的高度。

  • 創建模板cell
//返回一個template Cell
 - (__kindof UITableViewCell *)fd_templateCellForReuseIdentifier:(NSString *)identifier {
    NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier);
    //儲存單元格的字典。一種identifier對應一個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關聯。不過這個屬性的get方法似乎沒有被調用。使用 UITableViewCell 模板Cell計算高度,通過 fd_isTemplateLayoutCell 可在Cell內部判斷當前是否是模板Cell??梢允∪ヒ恍┡c高度無關的操作。
        templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO;
        templateCellsByIdentifiers[identifier] = templateCell;
        [self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]];
    }
    
    return templateCell;
}

fd_isTemplateLayoutCell屬性:模板cell僅用來計算高度,通過 fd_isTemplateLayoutCell 可在Cell內部判斷當前是否是模板Cell。若是模板cell可以省去一些與高度計算無關的操作。


  • templateCell高度計算
    - (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

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

關于UILable的問題

當 UILabel 行數大于0時,需要指定 preferredMaxLayoutWidth 后它才知道自己什么時候該折行。這是個“雞生蛋蛋生雞”的問題,因為 UILabel 需要知道 superview 的寬度才能折行,而 superview 的寬度還依仗著子 view 寬度的累加才能確定。

框架中的做法是:先計算contentView的寬度,然后對contentView添加寬度約束,然后使用systemLayoutSizeFittingSize:計算獲得高度,計算完成以后移除contentView的寬度約束。

CGFloat contentViewWidth = CGRectGetWidth(self.frame);//先設置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:并用數據內容來反算高度。
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.lxweimin.com/p/3f73e696dd4d

使用

注意的地方:
1.使用storyboard創建cell,要保證 contentView 內部上下左右所有方向都有約束支撐。

2.使用代碼或 XIB 創建的 cell,使用以下注冊方法:

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

3.cell通過-dequeueCellForReuseIdentifier:來創建。

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

- (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.不需要再設置estimatedRowHeight屬性

這里以使用autolayout的情況為例,使用frameLayout的情況不作說明,以下demo數據來自原作demo。

  • 使用storyboard
    UITableView+FDTemplateLayoutCell框架中的demo就是使用storyboard實現的,非常簡單易懂不作過多說明。下面說一些要注意的地方:
    在子線程解析json數據然后再回到主線程刷新tableView。以前自己一般的做法是設置一個NSMutableArray類型屬性用來儲存模型數據,然后在懶加載中解析數據。


imageview 的mode設置為aspect fit,在保持長寬比的前提下,縮放圖片,使得圖片在容器內完整顯示出來。


imageView注意1.png

然后imageView的右約束是一個不等于約束,intrinsic size 設為placeholder。這是因為:如果內容是運行時決定的如UIImageView,若圖片是從服務器下載的,那么我們就需要放一個空的UIImageView,不包含所顯示的圖片,不過這樣會因未設置圖片導致imageView尺寸無法確定,storyboard拋出錯誤,解決方案便是放一個臨時的占位尺寸來告訴sotryboard。


imageView注意2.png
  • 使用純代碼,autolayout
    參照storyboard約束設置用純代碼寫約束條件。
    自定義cell里面的實現:初始化的方法內部創建子控件并且使用Masonry布局



    initSubview方法的實現,保證 contentView 內部上下左右所有方向都有約束支撐:



    ps:這樣子設置約束還是會有點問題(包括原作的例子),想想如果標題、正文內容、圖片或者名字其中一個子控件賦值為空,但是約束仍然存在,這種情況下應該怎樣處理。
    更新:解決辦法。
 #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) {
        //以下設置距離contentView的邊距,設置兩條優先度不同的約束,內容為空時將優先度高的約束禁用
        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) {
        //以下設置距離title的邊距,設置兩條優先度不同的約束,內容為空時將優先度高的約束禁用
        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

控制器中的實現:基本和原作demo中的差不多,一定要使用- registerClass:forCellReuseIdentifier:方法注冊。而且應該像原作demo中在子線程解析json數據然后再回到主線程刷新tableView

有個奇怪的現象:如果vc中的數據模型是二維數組(section \row)的話只會計算、緩存一次高度。如果是一維數組,就會計算、緩存兩次高度(重復兩次)。不知道為什么。代碼如下:

 - (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 自動高度
優化UITableViewCell高度計算的那些事
UITableView+FDTemplateLayoutCell 框架學習
UITableView-FDTemplateLayoutCell源碼分析
有了Auto Layout,為什么你還是害怕寫UITabelView的自適應布局?
使用Autolayout實現UITableView的Cell動態布局和高度動態改變

更新:
關于UITableView+FDTemplateLayoutCell的1.2版本中利用RunLoop空閑時間執行預緩存任務(雖然預緩存功能因為下拉刷新的沖突和不明顯的收益已經廢棄)

sunny博客原文在這一部分已經講述得比較清楚了,這里總結一下
先來看看runloop內部邏輯:


RunLoop 內部的邏輯

預緩存高度 要求頁面處于空閑狀態時才執行高度計算,當用戶正在滑動列表時不應該執行計算任務影響滑動體驗,需要在最無感知的時刻進行,所以應該同時滿足:
1.RunLoop 處于“空閑”狀態(defaultMode)
2。當這一次 RunLoop 迭代處理完成了所有事件,馬上要休眠時

注冊 RunLoopObserver 可以觀測當前 RunLoop 的運行狀態,每個 Observer 都包含了一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能通過回調接受到這個變化??梢杂^測的時間點有以下幾個:

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

FD框架里面做的主要兩個事情:
1.創建observer觀測runloop即將進入休眠(kCFRunLoopBeforeWaiting),
2.在observer的回調里收集、分發任務(分發到多個runloop中執行避免卡主線程)。
利用performSelector這個api創建一個 Source 0 任務,分發到指定線程的 RunLoop 中,在給定的 Mode 下執行,若指定的 RunLoop 處于休眠狀態,則喚醒它處理事件(上面圖中第七步,source0任務可以喚醒runloop)

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

參考:
深入理解RunLoop
Cocoa深入學習:NSOperationQueue、NSRunLoop和線程安全

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,747評論 2 370

推薦閱讀更多精彩內容