TableView優(yōu)化之高度緩存

TableView優(yōu)化之高度緩存

系列文章:


吶跪腹,也好久沒寫博客了褂删,為什么呢?

因?yàn)閼邪 ?/h3>

不管你信與不信冲茸,這都不是真的屯阀。
因?yàn)樽罱镜捻?xiàng)目要上二版,然而我還沒有提前完成他的決心轴术,所以难衰,你懂得。

今天說點(diǎn)什么呢逗栽,恩盖袭,還是說說tabV相關(guān)的吧,之前的存貨了。


tableView優(yōu)化之高度緩存功能

日常開發(fā)中鳄虱,tableView的使用率很高弟塞,所以相對(duì)tableView的優(yōu)化來說可以做很多很多的事情。很多情況下拙已,我們的每一個(gè)cell都沒有一個(gè)固定的高度宣肚,而是根據(jù)cell里面的內(nèi)容自適應(yīng)高度的。那么每次當(dāng)我們cell將要出現(xiàn)在屏幕的時(shí)候悠栓,系統(tǒng)都會(huì)去計(jì)算cell的高度霉涨。如果說我能通過某種手段,在首次計(jì)算的時(shí)候惭适,將每個(gè)cell對(duì)應(yīng)的高度保存下載笙瑟,當(dāng)下次需要用到cell高度的時(shí)候再?gòu)谋4娴牡胤饺〕觯瑥亩鴾p少了計(jì)算量癞志,來達(dá)到優(yōu)化的目的往枷。

因此應(yīng)運(yùn)而生了這套高度緩存的算法。

在此聲明凄杯,這套算法不是老司機(jī)原創(chuàng)错洁,是整合并優(yōu)化了外國(guó)一位大神的源碼。

再次聲明戒突,

這原來是國(guó)人算法屯碴,揚(yáng)我國(guó)威啊,原型名
UITableView-FDTemplateLayoutCell膊存。


這篇博客中你可能會(huì)用到

  • category 导而、subclass、protocol之間的區(qū)別
  • 自動(dòng)布局相關(guān)
  • runTime動(dòng)態(tài)綁定
  • tableViewCell重用的相關(guān)

恩隔崎,其實(shí)東西并不多今艺,重要的還是一個(gè)思想。


老規(guī)矩爵卒,還是先放全部代碼虚缎。

#import "UITableView+HeightCache.h"
#import <objc/runtime.h>
@implementation UITableView (HeightCache)

#pragma mark ---接口方法---
-(CGFloat)DW_CalculateCellWithIdentifier:(NSString *)identifier
                               indexPath:(NSIndexPath *)indexPath
                           configuration:(void(^)(id cell))configuration
{
    if(self.bounds.size.width != 0)//防止初始寬度為0(如autoLayout初次加載時(shí))
    {
        if (!identifier.length || !indexPath) {//非空判斷
            return 0;
        }
        NSString * key = [self.cache makeKeyWithIdentifier:identifier indexPath:indexPath];//制作key
        if ([self.cache existInCacheByKey:key]) {//如果key存在
            return [self.cache heightFromCacheWithKey:key];//從字典中取出高
        }
        CGFloat height = [self DW_CalCulateCellWithIdentifier:identifier configuration:configuration];//不存在則計(jì)算高度
        [self.cache cacheHeight:height byKey:key];//并緩存
        return height;
    }
    return 0;
}
-(void)DW_RemoveHeightCacheWithIdentifier:(NSString *)identifier
                                indexPath:(NSIndexPath *)indexPath
                             numberOfRows:(NSInteger)rows
{
    [self.cache removeHeightByIdentifier:identifier indexPath:indexPath numberOfRows:rows];
}
-(void)DW_RemoveAllHeightCache
{
    [self.cache removeAllHeight];
}
-(void)DW_InsertCellToIndexPath:(NSIndexPath *)indexPath
                 withIdentifier:(NSString *)identifier
                   numberOfRows:(NSInteger)rows
{
    [self.cache insertCellToIndexPath:indexPath withIdentifier:identifier numberOfRows:rows toDictionaryForCache:self.cache.dicHeightCurrent];
}
-(void)DW_MoveCellFromIndexPath:(NSIndexPath *)sourceIndexPath
    sourceIndexPathNumberOfRows:(NSInteger)sourceRows
                    toIndexPath:(NSIndexPath *)destinationIndexPath
destinationIndexPathNumberOfRows:(NSInteger)destinationRows
                 withIdentifier:(NSString *)identifier
{
    [self.cache moveCellFromIndexPath:sourceIndexPath sourceSectionNumberOfRows:sourceRows toIndexPath:destinationIndexPath destinationSectionNumberOfRows:destinationRows withIdentifier:identifier];
}
#pragma mark ---工具方法---
///從重用池中返回計(jì)算用的cell
-(__kindof UITableViewCell  *)DW_CalculateCellWithIdentifier:(NSString *)identifier
{
    if (!identifier.length) {
        return nil;
    }
    NSMutableDictionary <NSString * ,UITableViewCell *> *DicForTheUniqueCalCell = objc_getAssociatedObject(self, _cmd);//利用runtime取出tableV綁定的存有cell的字典
    if (!DicForTheUniqueCalCell) {
        DicForTheUniqueCalCell = [NSMutableDictionary dictionary];//如果取不到則新建并綁定
        objc_setAssociatedObject(self, _cmd, DicForTheUniqueCalCell, OBJC_ASSOCIATION_RETAIN_NONATOMIC);//動(dòng)態(tài)綁定(綁定目標(biāo),關(guān)鍵字钓株,綁定者实牡,策略)
    }
    //以上只是為了只綁定一個(gè)字典,類比懶加載
    UITableViewCell * cell = DicForTheUniqueCalCell[identifier];
    if (!cell) {
        cell = [self dequeueReusableCellWithIdentifier:identifier];//從重用池中取一個(gè)cell用來計(jì)算享幽,必須以本方式從重用池中取铲掐,若以indexPath方式取由于-heightForRowAtIndexPath方法會(huì)造成循環(huán)拾弃。
        cell.contentView.translatesAutoresizingMaskIntoConstraints = NO;//開啟約束
        cell.JustForCal = YES;//標(biāo)記只用來計(jì)算
        DicForTheUniqueCalCell[identifier] = cell;
    }
    //同上值桩,保證只有一個(gè)用來計(jì)算的cell
    return cell;
}

///根據(jù)重用表示取出cell并操作cell后,計(jì)算高度
-(CGFloat)DW_CalCulateCellWithIdentifier:(NSString *)identifier
                           configuration:(void(^)(id cell))configuration
{
    if (!identifier.length) {
        return 0;
    }
    UITableViewCell * cell = [self DW_CalculateCellWithIdentifier:identifier];
    [cell prepareForReuse];//放回重用池
    if (configuration) {
        configuration(cell);//對(duì)cell進(jìn)行操作
    }    
    return [self DW_CalculateCellHeightWithCell:cell];
}

///根據(jù)cell計(jì)算cell的高度
-(CGFloat)DW_CalculateCellHeightWithCell:(UITableViewCell *)cell
{
    CGFloat width = self.bounds.size.width;
    //根據(jù)輔助視圖校正width
    if (cell.accessoryView) {
        width -= cell.accessoryView.bounds.size.width + 16;
    }
    else
    {
        static const CGFloat accessoryWidth[] = {
            [UITableViewCellAccessoryNone] = 0,
            [UITableViewCellAccessoryDisclosureIndicator] = 34,
            [UITableViewCellAccessoryDetailDisclosureButton] = 68,
            [UITableViewCellAccessoryCheckmark] = 40,
            [UITableViewCellAccessoryDetailButton] = 48
        };
        width -= accessoryWidth[cell.accessoryType];
    }
    CGFloat height = 0;
    if (!cell.NoAutoSizing && width > 0) {//如果不是非自適應(yīng)模式則添加約束后計(jì)算約束后高度
        NSLayoutConstraint * widthConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:width];//創(chuàng)建約束
        [cell.contentView addConstraint:widthConstraint];//添加約束
        height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;//計(jì)算高度
        [cell.contentView removeConstraint:widthConstraint];//移除約束
    }
    if (height == 0) {//如果約束錯(cuò)誤可能導(dǎo)致計(jì)算結(jié)果為零豪椿,則以自適應(yīng)模式再次計(jì)算
        height = [cell sizeThatFits:CGSizeMake(width, 0)].height;
    }
    if (height == 0) {//如果計(jì)算仍然為0奔坟,則給出默認(rèn)高度
        height = 44;
    }
    if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {//如果不為無分割線模式則添加分割線高度
        height += 1.0 /[UIScreen mainScreen].scale;
    }
    return height;
}

#pragma mark ---setter携栋、getter---
-(HeightCache *)cache//懶加載形式
{
    HeightCache * cacheTemp = objc_getAssociatedObject(self, _cmd);
    if (!cacheTemp) {
        cacheTemp = [HeightCache new];
        objc_setAssociatedObject(self, _cmd, cacheTemp, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return cacheTemp;
}
-(void)setCache:(HeightCache *)cache
{
    objc_setAssociatedObject(self, @selector(cache), cache, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end


@implementation UITableViewCell (HeightCacheCell)
#pragma mark ---setter、getter---
-(BOOL)NoAutoSizing
{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}
-(void)setNoAutoSizing:(BOOL)NoAutoSizing
{
    objc_setAssociatedObject(self, @selector(NoAutoSizing), @(NoAutoSizing), OBJC_ASSOCIATION_RETAIN);//關(guān)鍵字用getter的方法名咳秉,為保持關(guān)鍵字一致
}
-(BOOL)JustForCal
{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}
-(void)setJustForCal:(BOOL)JustForCal
{
    objc_setAssociatedObject(self, @selector(JustForCal), @(JustForCal), OBJC_ASSOCIATION_RETAIN);
}
@end

@implementation HeightCache
///制作key
-(NSString *)makeKeyWithIdentifier:(NSString *)identifier
                         indexPath:(NSIndexPath *)indexPath
{
    return [NSString stringWithFormat:@"%@S%ldR%ld",identifier,indexPath.section,indexPath.row];
}

///高度是否存在
-(BOOL)existInCacheByKey:(NSString *)key
{
    NSNumber * value = [self.dicHeightCurrent valueForKey:key];
    return (value && ![value isEqualToNumber:@-1]);
}

///取出緩存的高度
-(CGFloat)heightFromCacheWithKey:(NSString *)key
{
    NSNumber * value = [self.dicHeightCurrent valueForKey:key];
    if ([self is64bit]) {
        return [value doubleValue];
    }
    return [value floatValue];
}

///64位判斷
- (BOOL)is64bit
{
#if defined(__LP64__) && __LP64__
    return YES;
#else
    return NO;
#endif
}

///高度緩存
-(void)cacheHeight:(CGFloat)height
             byKey:(NSString *)key
{
    [self.dicHeightCurrent setValue:@(height) forKey:key];
}

///根據(jù)key刪除緩存
-(void)removeHeightByIdentifier:(NSString *)identifier
                      indexPath:(NSIndexPath *)indexPath
                   numberOfRows:(NSInteger)rows
{
    if (indexPath.row < rows) {
        for (int i = 0; i < rows - 1 - indexPath.row; i ++) {
            NSIndexPath * indexPathA = [NSIndexPath indexPathForRow:indexPath.row + i inSection:indexPath.section];
            NSLog(@"%ld,%ld",indexPathA.row,indexPathA.section);
            NSIndexPath * indexPathB = [NSIndexPath indexPathForRow:indexPath.row + i + 1 inSection:indexPath.section];
            NSLog(@"%ld,%ld",indexPathB.row,indexPathB.section);
            [self exchangeValueForIndexPathA:indexPathA andIndexPathB:indexPathB withIdentifier:identifier dictionary:self.dicHeightCacheH];
            [self exchangeValueForIndexPathA:indexPathA andIndexPathB:indexPathB withIdentifier:identifier dictionary:self.dicHeightCacheV];
        }
        NSIndexPath * indexPathC = [NSIndexPath indexPathForRow:rows - 1 inSection:indexPath.section];
        NSString * key = [self makeKeyWithIdentifier:identifier indexPath:indexPathC];
        [self.dicHeightCacheH removeObjectForKey:key];
        [self.dicHeightCacheV removeObjectForKey:key];
    }
}

///刪除所有緩存
-(void)removeAllHeight
{
    [self.dicHeightCacheH removeAllObjects];
    [self.dicHeightCacheV removeAllObjects];
}

///插入cell是插入value
-(void)insertCellToIndexPath:(NSIndexPath *)indexPath
            withNumberOfRows:(NSInteger)rows
                heightNumber:(NSNumber *)height
                  identifier:(NSString *)identifier
        toDictionaryForCache:(NSMutableDictionary *)dic
{
    if (indexPath.row < rows + 1) {
        [self insertCellToIndexPath:indexPath withIdentifier:identifier numberOfRows:rows toDictionaryForCache:dic];
        NSString * key = [self makeKeyWithIdentifier:identifier indexPath:indexPath];
        [dic setValue:height forKey:key];
    }
}
-(void)insertCellToIndexPath:(NSIndexPath *)indexPath withIdentifier:(NSString *)identifier numberOfRows:(NSInteger)rows toDictionaryForCache:(NSMutableDictionary *)dic
{
    if (indexPath.row < rows + 1) {
        for (int i = 0; i < rows - indexPath.row; i ++) {
            NSIndexPath * indexPathA = [NSIndexPath indexPathForRow:rows - i inSection:indexPath.section];
            NSIndexPath * indexPathB = [NSIndexPath indexPathForRow:rows - i - 1 inSection:indexPath.section];
            [self exchangeValueForIndexPathA:indexPathA andIndexPathB:indexPathB withIdentifier:identifier dictionary:dic];
        }
    }
}
///移動(dòng)cell時(shí)交換value
-(void)moveCellFromIndexPath:(NSIndexPath *)sourceIndexPath
   sourceSectionNumberOfRows:(NSInteger)sourceRows
                 toIndexPath:(NSIndexPath *)destinationIndexPath
destinationSectionNumberOfRows:(NSInteger)destinationRows
              withIdentifier:(NSString *)identifier
{
    if (sourceIndexPath.section == destinationIndexPath.section) {
        [self moveCellInSectionFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath withIdentifier:identifier];
    }
    else
    {
        [self moveCellOutSectionFromIndexPath:sourceIndexPath sourceSectionNumberOfRows:sourceRows toIndexPath:destinationIndexPath destinationSectionNumberOfRows:destinationRows withIdentifier:identifier];
    }
}
///組內(nèi)移動(dòng)
-(void)moveCellInSectionFromIndexPath:(NSIndexPath *)sourceIndexPath
                          toIndexPath:(NSIndexPath *)destinationIndexPath
                       withIdentifier:(NSString *)identifier
{
    NSInteger rowA = sourceIndexPath.row;
    NSInteger rowB = destinationIndexPath.row;
    for (int i = 0; i < (MAX(rowA, rowB) - MIN(rowA, rowB)); i ++) {
        NSIndexPath * indexPathA = [NSIndexPath indexPathForRow:MIN(rowA, rowB) + i inSection:sourceIndexPath.section];
        NSIndexPath * indexPathB = [NSIndexPath indexPathForRow:MIN(rowA, rowB) + i + 1 inSection:sourceIndexPath.section];
        [self exchangeValueForIndexPathA:indexPathA andIndexPathB:indexPathB withIdentifier:identifier dictionary:self.dicHeightCacheV];
        [self exchangeValueForIndexPathA:indexPathA andIndexPathB:indexPathB withIdentifier:identifier dictionary:self.dicHeightCacheH];
    }
}
///組外移動(dòng)
-(void)moveCellOutSectionFromIndexPath:(NSIndexPath *)sourceIndexPath
             sourceSectionNumberOfRows:(NSInteger)sourceRows
                           toIndexPath:(NSIndexPath *)destinationIndexPath
        destinationSectionNumberOfRows:(NSInteger)destinationRows
                        withIdentifier:(NSString *)identifier
{
    NSNumber * numberH;
    NSNumber * numberV;
    NSLog(@"%ld",sourceIndexPath.row);
    if (sourceIndexPath.row < sourceRows) {
        NSString * key = [self makeKeyWithIdentifier:identifier indexPath:sourceIndexPath];
        numberH = self.dicHeightCacheH[key];
        numberV = self.dicHeightCacheV[key];
        [self removeHeightByIdentifier:identifier indexPath:sourceIndexPath numberOfRows:sourceRows];
    }
    NSLog(@"%ld,%ld",destinationIndexPath.row,destinationIndexPath.section);
    [self insertCellToIndexPath:destinationIndexPath withNumberOfRows:destinationRows heightNumber:numberH identifier:identifier toDictionaryForCache:self.dicHeightCacheH];
    [self insertCellToIndexPath:destinationIndexPath withNumberOfRows:destinationRows heightNumber:numberV identifier:identifier toDictionaryForCache:self.dicHeightCacheV];
}
///根據(jù)indexPath交換兩個(gè)Key
-(void)exchangeValueForIndexPathA:(NSIndexPath *)indexPathA
                    andIndexPathB:(NSIndexPath *)indexPathB
                   withIdentifier:(NSString *)identifier
                       dictionary:(NSMutableDictionary *)dic
{
    NSString * keyA = [self makeKeyWithIdentifier:identifier indexPath:indexPathA];
    NSString * keyB = [self makeKeyWithIdentifier:identifier indexPath:indexPathB];
    NSNumber * Temp = dic[keyA];
    dic[keyA] = dic[keyB];
    dic[keyB] = Temp;
}
#pragma mark ---懶加載---
-(NSMutableDictionary *)dicHeightCacheH
{
    if (!_dicHeightCacheH) {
        _dicHeightCacheH = [NSMutableDictionary dictionary];
    }
    return _dicHeightCacheH;
}
-(NSMutableDictionary *)dicHeightCacheV
{
    if (!_dicHeightCacheV) {
        _dicHeightCacheV = [NSMutableDictionary dictionary];
    }
    return _dicHeightCacheV;
}
-(NSMutableDictionary *)dicHeightCurrent//根據(jù)系統(tǒng)狀態(tài)返回對(duì)應(yīng)字典
{
    return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation)?self.dicHeightCacheV:self.dicHeightCacheH;
}
@end

這么多婉支,你這個(gè)騙子!
你是不是這么想得澜建。

別慌向挖,東西不多,因?yàn)檫@是一個(gè)category炕舵,復(fù)用性非常高何之,所以老司機(jī)想盡量減少文件個(gè)數(shù),這樣集成的時(shí)候也方便不是咽筋。

所以溶推,老司機(jī)把三個(gè)類寫在一個(gè)文件里了

之前有人問過我把幾個(gè)類寫在一個(gè)文件中有什么好處么奸攻?

老司機(jī)目前只能說蒜危,除了看上去裝逼點(diǎn),別的沒什么卵用睹耐。

可能是老司機(jī)的理解不深辐赞,不過為了集成方便我還是寫在一個(gè)文件里了。

廢話不多說硝训,分段講解吧占拍。


分段解析

為什么選擇category而不是subclass

我想很多人都會(huì)有疑問,為什么選擇category而不是繼承捎迫,畢竟category不能添加屬性,用起來不是很方便晃酒。說到這又要老生常談了。

Protocol只是聲明一套接口窄绒,并不能提供具體實(shí)現(xiàn)贝次,變相的也算是一種抽象基類的實(shí)現(xiàn)方式(OC本身語(yǔ)法并不支持抽象基類)。

Category可以為已有的類提供額外的接口和具體的實(shí)現(xiàn)彰导。

Protocol只能提供一套公用的接口聲明蛔翅,并不能提供具體實(shí)現(xiàn),它的行為是位谋,我只負(fù)責(zé)聲明山析,而不管誰(shuí)去實(shí)現(xiàn),去如何實(shí)現(xiàn)掏父。這樣的話笋轨,我定義一套接口,可以使任意的類都用不同的方式去實(shí)現(xiàn)接口中的方法,就是為遵守了protocol的類提供了一些額外訪問這個(gè)類的一些接口爵政,像delegate和dataSource用protocol實(shí)現(xiàn)是最好的仅讽。

Category是對(duì)一個(gè)功能完備的類的一種補(bǔ)充、擴(kuò)展钾挟,就像一個(gè)東西基本功能都完成了洁灵,可以用category為這個(gè)類添加不同的組件,使得這個(gè)類能夠適應(yīng)不同情況的需求(但是這些不同需求最核心的需求要一致)掺出。當(dāng)然徽千,當(dāng)某個(gè)類非常大的時(shí)候,使用category可以按照不同的功能將類的實(shí)現(xiàn)分在不同的模塊中汤锨。還有罐栈,雖然category可以訪問已有類的實(shí)例變量,但不能創(chuàng)建新的實(shí)例變量泥畅,如果要?jiǎng)?chuàng)建新的實(shí)例變量荠诬,請(qǐng)使用繼承。

繼承位仁,它基于Protocol和Category之間柑贞,既可以像protocol一樣只提供純粹的接口,也可以像Category一樣提供接口的完整實(shí)現(xiàn)聂抢,可以自由定義類的實(shí)例變量(這一點(diǎn)钧嘶,Protocol倒是可以聲明實(shí)例變量,但是也僅僅是聲明而已)琳疏,而且繼承還可以對(duì)類以后的方法進(jìn)行改寫有决,所以繼承的力量是最強(qiáng)大的。

在iOS開發(fā)中空盼,繼承是完全可以完成protocol和category的功能的书幕,那么在開發(fā)過程中多多使用繼承體系可好?

需要注意的是使用繼承還有很大的代價(jià)問題揽趾。使用繼承來進(jìn)行擴(kuò)展是一種耦合度很高的行為台汇,對(duì)父類可以說是完全依賴,如果繼承體系太過復(fù)雜篱瞎,會(huì)造成難以維護(hù)的問題苟呐。如果僅僅只是對(duì)類進(jìn)行擴(kuò)展,并不建議使用繼承俐筋,畢竟使用protocol和category是很簡(jiǎn)單牵素、輕松的。除此之外澄者,在開發(fā)過程中笆呆,我們應(yīng)該盡量將界面请琳、功能相似的類的代碼提取到基類里面,然后各個(gè)子類繼承自這個(gè)基類腰奋,實(shí)現(xiàn)各自的其他特殊部分单起。這樣可以大大的優(yōu)化代碼抱怔,如果需要修改的話劣坊,只需要這倒對(duì)應(yīng)子類修改即可。

是不是感覺老司機(jī)屌屌的屈留,嘖嘖嘖局冰,我百度的。

我選擇category就一個(gè)原因灌危,擴(kuò)展方便康二,二次開發(fā)也方便。


類目拆解

老司機(jī)說過勇蝙,這一坨代碼是三個(gè)類寫在了一個(gè)文件里沫勿,他們都是什么呢?

  • UITableView的category
  • UITableViewCell的category
  • 一個(gè)NSObject的子類

他們分別是做什么的呢味混?

首先产雹,給UITableView添加category是因?yàn)闉榱?code>實(shí)現(xiàn)高度緩存,我的方案是在計(jì)算高度的時(shí)候就模擬數(shù)據(jù)填充翁锡,從而計(jì)算出該cell的高度蔓挖,所以,tableView應(yīng)該有填充數(shù)據(jù)和計(jì)算高度的方法馆衔。故為其添加分類瘟判。

而那個(gè)繼承于NSObject的子類就是用來存儲(chǔ)計(jì)算出來的高度的,這樣當(dāng)下次需要計(jì)算的時(shí)候直接從這里取出即可角溃。

至于那個(gè)UITableViewCell的類目是為了給cell添加兩個(gè)標(biāo)識(shí)符拷获,一個(gè)用來判斷當(dāng)前cell是否需要一autolayout進(jìn)行繪制,另一個(gè)是用來區(qū)分計(jì)算用的cell還是展示用的cell减细。這點(diǎn)現(xiàn)在可能你還不懂刀诬,一會(huì)我們會(huì)做相應(yīng)介紹。


UITableViewCell的category

為什么先說這個(gè)類目呢邪财,因?yàn)檫@個(gè)類目的內(nèi)容最少陕壹,而且只用到了runtime的動(dòng)態(tài)綁定,可以拿出來單獨(dú)介紹一下runtime的相關(guān)知識(shí)树埠。

什么是runtime

run糠馆,運(yùn)行,time怎憋,時(shí)間又碌,那么runtime就是運(yùn)行時(shí)九昧。恩,簡(jiǎn)單不毕匀?
然后我們說說铸鹰。。皂岔。誒誒誒蹋笼,別打別打,開個(gè)玩笑躁垛。
runtime剖毯,簡(jiǎn)稱運(yùn)行時(shí),是系統(tǒng)在運(yùn)行期間的一些機(jī)制教馆。而對(duì)于OC來講呢逊谋,其中最重要的就是消息機(jī)制

C語(yǔ)言呢土铺,我們調(diào)用函數(shù)胶滋,在編譯期就已經(jīng)確定了要調(diào)用那個(gè)函數(shù),而且整個(gè)過程是順序執(zhí)行的悲敷。

而在OC中呢究恤,我們是講消息發(fā)送的。而且我們是等待某個(gè)信號(hào)觸發(fā)才執(zhí)行代碼的镀迂。我們知道OC事實(shí)上是基于C的丁溅,那他是怎么實(shí)現(xiàn)這套轉(zhuǎn)換的呢?就是通過runtime去實(shí)現(xiàn)的探遵。

不信窟赏?不信跟我來做個(gè)試驗(yàn)。

新開一個(gè)工程箱季,刪掉所有文件涯穷,只留下info.plist和main.m。并且將引入的頭文件刪除掉藏雏。

建一個(gè)類拷况,里面隨便寫一個(gè)方法的聲明和實(shí)現(xiàn)。

然后在main.m中引入這個(gè)類掘殴,初始化并調(diào)用剛才聲明的方法赚瘦,如下圖。

新建一個(gè)工程

此時(shí)奏寨,打開我們的終端起意。找到剛才的工程的main.m,并且輸入
clang -rewrite-objc main.m,點(diǎn)擊回車病瞳。稍等你就會(huì)看到提示轉(zhuǎn)換完成揽咕。

轉(zhuǎn)化我們的文件

這回在finder中找到工程的文件夾缘回,在main.m同級(jí)文件夾下多了一個(gè)文件main.cpp懂扼,這就是轉(zhuǎn)換完的文件。我們看到代碼還是很多的萄唇。直接拖到最下方我們大概能看到點(diǎn)認(rèn)識(shí)的了般堆,int main诸狭。专甩。遮晚。

這就是我們剛才main函數(shù)里面的實(shí)現(xiàn)

找到實(shí)現(xiàn)

看不懂掘而?待我?guī)湍戕坜坌冢サ粢恍╊愋娃D(zhuǎn)換用的修飾符后剩下如下代碼于购,是不是清晰多了袍睡?

姑且就叫化簡(jiǎn)吧

先看被我框選中的代碼,objc_msgSend是說發(fā)送消息肋僧,他有兩個(gè)參數(shù)斑胜,一個(gè)是實(shí)例,一個(gè)是方法嫌吠。objc_getClass通過字符串獲取到這個(gè)類止潘。sel_registerName通過字符串獲取方法。所以這句話的意思就是給這個(gè)類發(fā)送了這個(gè)消息辫诅,消息內(nèi)容就是一個(gè)方法凭戴。

隨后就容易了,給這個(gè)實(shí)例發(fā)送一個(gè)sayHello的消息炕矮,參數(shù)是后面的字符串么夫。

通過這里我們知道我們OC的語(yǔ)言是怎么實(shí)現(xiàn)的了吧,就是通過runtime轉(zhuǎn)化成了C++的代碼肤视,然后進(jìn)行運(yùn)行档痪。

從這你也應(yīng)該知道為什么OC中叫發(fā)送消息,不叫函數(shù)調(diào)用了吧邢滑。

另外你還應(yīng)該知道為什么OC中方法只聲明腐螟,不實(shí)現(xiàn)編譯時(shí)只報(bào)警不報(bào)錯(cuò)困后,運(yùn)行時(shí)crash是為什么了吧乐纸。

既然說到這里就多少說一說C與OC吧。之前老司機(jī)說過摇予,OC是基于C的汽绢,那么C語(yǔ)言中是沒有對(duì)象這個(gè)概念呢,我們的對(duì)象又是什么呢趾盐?

右手啊庶喜,程序員哪有什么對(duì)象小腊,嘖嘖嘖。

除了右手久窟,還有結(jié)構(gòu)體秩冈,OC的對(duì)象就是C語(yǔ)言中的結(jié)構(gòu)體

對(duì)象的結(jié)構(gòu)

我們看到了斥扛,每個(gè)類都是一個(gè)都是一個(gè)結(jié)構(gòu)體入问,其中有各種指針,指向一個(gè)類的各種參數(shù)稀颁,父類芬失、屬性列表、方法列表等等匾灶。

所以說當(dāng)我們聲明了類的方法棱烂,方法列表里面就有這個(gè)方法了,然后編譯通過了阶女,然后調(diào)用的時(shí)候颊糜,方法選擇器去本類的方法列表里去尋找方法的實(shí)現(xiàn),如果沒有實(shí)現(xiàn)秃踩,則去其父類中尋找衬鱼,如果在沒有通過一系列消息轉(zhuǎn)發(fā)機(jī)制會(huì)一直找下去,直到最后也沒有找到這個(gè)方法的實(shí)現(xiàn)就crash了憔杨。關(guān)于消息轉(zhuǎn)發(fā)鸟赫,其實(shí)還有很多東西,但是在這里講就又扯遠(yuǎn)了消别,所以等下期吧=抛蚤。=

本例中我們用runtime做了些什么呢?

使用runtime動(dòng)態(tài)為category綁定屬性

之前老司機(jī)說過妖啥,category是不能添加屬性的霉颠。那我又要為其添加兩個(gè)標(biāo)識(shí)符,只能使用runtime去動(dòng)態(tài)綁定了荆虱,在類的屬性列表里面通過runtime添加上這個(gè)屬性蒿偎,那我就可以使用這個(gè)屬性了。

首先在.h的UITableCell的category的@interface之中添加兩個(gè)屬性

@interface UITableViewCell (HeightCacheCell)
@property (assign ,nonatomic)BOOL JustForCal;//計(jì)算用的cell標(biāo)識(shí)符(將計(jì)算用的cell與正常顯示的cell進(jìn)行區(qū)分怀读,避免不必要的ui響應(yīng))
@property (assign ,nonatomic)BOOL NoAutoSizing;//不適用autoSizing標(biāo)識(shí)符(不依靠約束計(jì)算诉位,只進(jìn)行自適應(yīng))
@end

這是我為期添加的兩個(gè)屬性,具體有什么作用菜枷,下文中會(huì)提到的苍糠,先別急。

然后在.m中添加兩個(gè)屬性的setter啤誊、getter方法

@implementation UITableViewCell (HeightCacheCell)
#pragma mark ---setter岳瞭、getter---
-(BOOL)NoAutoSizing
{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}
-(void)setNoAutoSizing:(BOOL)NoAutoSizing
{
    objc_setAssociatedObject(self, @selector(NoAutoSizing), @(NoAutoSizing), OBJC_ASSOCIATION_RETAIN);//關(guān)鍵字用getter的方法名拥娄,為保持關(guān)鍵字一致
}
-(BOOL)JustForCal
{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}
-(void)setJustForCal:(BOOL)JustForCal
{
    objc_setAssociatedObject(self, @selector(JustForCal), @(JustForCal), OBJC_ASSOCIATION_RETAIN);
}
@end

這里老司機(jī)有必要說一下兩個(gè)方法

  • objc_setAssociatedObject
  • objc_getAssociatedObject

OC自解釋語(yǔ)言的好處就體現(xiàn)出來了,從函數(shù)名你就可以看出來瞳筏,一個(gè)是給對(duì)象設(shè)置聯(lián)系稚瘾,一個(gè)是從對(duì)象獲取聯(lián)系。反正我英語(yǔ)水平就這樣姚炕,我也沒查字典摊欠,對(duì)不對(duì)的我就不深究了。一個(gè)setter一個(gè)getter就在這柱宦。

objc_setAssociatedObject

objc_setAssociatedObject(self, @selector(NoAutoSizing), @(NoAutoSizing), OBJC_ASSOCIATION_RETAIN);

方法總共四個(gè)參數(shù)些椒,分別是綁定目標(biāo),關(guān)鍵字掸刊,綁定者免糕,策略

所以說簡(jiǎn)單了痒给,綁定目標(biāo)说墨,就是給誰(shuí)綁定骏全,當(dāng)然是UITableViewCell這個(gè)category了苍柏,所以self。

綁定的關(guān)鍵字就是說我給這個(gè)對(duì)象綁定一個(gè)屬性姜贡,我總要有一個(gè)標(biāo)示符去表示那個(gè)屬性吧试吁,這樣我要調(diào)用這個(gè)屬性的時(shí)候通過標(biāo)示符去尋找才能找到這個(gè)屬性。

綁定者楼咳,就是我們要為這個(gè)屬性綁定的值了熄捍。

綁定策略就是說綁定的這個(gè)屬性的引用機(jī)制了。這里要說明一點(diǎn)母怜,這個(gè)綁定策略如何選擇余耽,老司機(jī)目前也沒有搞懂,所以策略這里都沿用了原作者的寫法苹熏,等老司機(jī)搞懂了之后會(huì)告訴大家的碟贾。

objc_getAssociatedObject

兩個(gè)參數(shù),一個(gè)綁定目標(biāo)轨域,一個(gè)關(guān)鍵字袱耽,通過關(guān)鍵字從綁定目標(biāo)中獲取屬性的值。

這下是不是明白這兩個(gè)setter干发、getter方法的意義了朱巨。

好了,這個(gè)category講完了枉长,他的東西真的很少冀续。

什么琼讽,你敲不出來這兩個(gè)方法?
忘了講了洪唐,你沒引入頭文件跨琳。。桐罕。

import <objc/runtime.h>


HeightCache

為什么說這個(gè)類呢脉让?怎么還不進(jìn)入正題呢?說好的UITableView的category呢功炮?

因?yàn)檫@個(gè)類是負(fù)責(zé)存儲(chǔ)Cell高度的類溅潜,而UITableView得category只是為獲取cell高度提供了一個(gè)接口,當(dāng)我們移動(dòng)cell薪伏,添加cell滚澜,刪除cell的時(shí)候要對(duì)這個(gè)高度的對(duì)應(yīng)關(guān)系作出很多的操作,UITableView的category中大量的使用了這里的方法嫁怀,所以老司機(jī)決定先把難啃的骨頭解決了设捐。

在.h中添加三個(gè)字典

@property (strong ,nonatomic)NSMutableDictionary * dicHeightCacheV;//豎直行高緩存字典
@property (strong ,nonatomic)NSMutableDictionary * dicHeightCacheH;//水平行高緩存字典
@property (strong ,nonatomic)NSMutableDictionary * dicHeightCurrent;//當(dāng)前狀態(tài)行高緩存字典(中間量)

為什么三個(gè)字典呢?老司機(jī)是這樣考慮的塘淑,橫屏和豎屏情況下同樣內(nèi)容的cell有可能是不同的萝招,如果以同一個(gè)高度去取得話有可能出現(xiàn)高度不準(zhǔn)確的問題。所以豎屏橫屏分別一個(gè)字典存捺。那這個(gè)current又是什么呢槐沼?就是自動(dòng)返回當(dāng)前屏幕狀態(tài)所對(duì)應(yīng)的字典那么一個(gè)中間量,這樣我們寫代碼的時(shí)候可以不用考慮當(dāng)前屏幕狀態(tài)而統(tǒng)一使用current這個(gè)字典捌治,減少很多代碼量岗钩。

去.m中看看是如何實(shí)現(xiàn)的。

#pragma mark ---懶加載---
-(NSMutableDictionary *)dicHeightCacheH
{
    if (!_dicHeightCacheH) {
        _dicHeightCacheH = [NSMutableDictionary dictionary];
    }
    return _dicHeightCacheH;
}
-(NSMutableDictionary *)dicHeightCacheV
{
    if (!_dicHeightCacheV) {
        _dicHeightCacheV = [NSMutableDictionary dictionary];
    }
    return _dicHeightCacheV;
}
-(NSMutableDictionary *)dicHeightCurrent//根據(jù)系統(tǒng)狀態(tài)返回對(duì)應(yīng)字典
{
    return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation)?self.dicHeightCacheV:self.dicHeightCacheH;
}

上面兩個(gè)字典是懶加載肖油,不多說了兼吓,在需要的時(shí)候創(chuàng)建字典。

重點(diǎn)是這個(gè)current森枪。

UIDeviceOrientationIsPortrait()這個(gè)方法是判斷括號(hào)中的狀態(tài)是否是豎屏狀態(tài)的一個(gè)方法视搏,所以括號(hào)里面我們給他當(dāng)前屏幕狀態(tài)他就可以判斷是不是豎屏了。

[UIDevice currentDevice].orientation而這句代碼見名知意疲恢,取出當(dāng)前設(shè)備的屏幕狀態(tài)凶朗。

讓后通過三目運(yùn)算符返回相應(yīng)的字典。

是不是寫法上很簡(jiǎn)單显拳,實(shí)際使用過程中也很方便棚愤。

上面幾個(gè)工具方法最主要的主要由如下幾個(gè)方法,交換兩個(gè)cell高度的值,插入一個(gè)cell高度的值宛畦,刪除一個(gè)cell高度的值瘸洛。通過這三個(gè)最基本的方法組合出所有cell操作需要用到的方法。算法都很基礎(chǔ)次和,沒什么需要說的反肋。


UITableView的category

最后的主角來了。其實(shí)你會(huì)發(fā)現(xiàn)這里的方法并不多踏施。因?yàn)橹皇窍蛲饨缣峁┝瞬迦胧幔瑒h除,刪除全部畅形,移動(dòng)养距,計(jì)算高度五個(gè)接口。

我們看一下大體思路:

大體思路

老司機(jī)覺得自己畫這圖也是沒誰(shuí)了日熬。棍厌。。

.h中添加一個(gè)屬性竖席,是我們剛才用來存儲(chǔ)高度的那個(gè)類的一個(gè)實(shí)例耘纱。

@property (strong ,nonatomic)HeightCache * cache;//緩存實(shí)例

其實(shí)你完全可以寫在.m里當(dāng)做一個(gè)私有變量去處理,這樣也更安全一些毕荐。
老司機(jī)寫在這里是為了調(diào)試的時(shí)候更直觀的看到緩存高度操作時(shí)的狀態(tài)束析。實(shí)際應(yīng)用中,如無特殊需要东跪,建議將其寫在.m中畸陡。

.m中,我們先看一下這幾個(gè)工具方法虽填,這才是核心部分。接口方法都是簡(jiǎn)單調(diào)用這幾個(gè)工具方法曹动,供外界調(diào)用的斋日。


  • -(__kindof UITableViewCell *)DW_CalculateCellWithIdentifier:(NSString *)identifier
  • -(CGFloat)DW_CalculateCellHeightWithCell:(UITableViewCell *)cell
  • -(CGFloat)DW_CalCulateCellWithIdentifier:(NSString *)identifier configuration:(void(^)(id cell))configuration

核心算法都在這了,我一定會(huì)好好解析的墓陈。(第一個(gè)方法返回值有一個(gè)*號(hào)我敲不出來恶守,不知道m(xù)arkdown什么鬼沖突。贡必。)

-(__kindof UITableViewCell *)DW_CalculateCellWithIdentifier:(NSString *)identifier

第一個(gè)方法如下:

///從重用池中返回計(jì)算用的cell
-(__kindof UITableViewCell *)DW_CalculateCellWithIdentifier:(NSString *)identifier
{
    if (!identifier.length) {
        return nil;
    }
    NSMutableDictionary <NSString * ,UITableViewCell *> *DicForTheUniqueCalCell = objc_getAssociatedObject(self, _cmd);//利用runtime取出tableV綁定的存有cell的字典
    if (!DicForTheUniqueCalCell) {
        DicForTheUniqueCalCell = [NSMutableDictionary dictionary];//如果取不到則新建并綁定
        objc_setAssociatedObject(self, _cmd, DicForTheUniqueCalCell, OBJC_ASSOCIATION_RETAIN_NONATOMIC);//動(dòng)態(tài)綁定(綁定目標(biāo)兔港,關(guān)鍵字,綁定者仔拟,策略)
    }
    //以上只是為了只綁定一個(gè)字典衫樊,類比懶加載
    UITableViewCell * cell = DicForTheUniqueCalCell[identifier];
    if (!cell) {
        cell = [self dequeueReusableCellWithIdentifier:identifier];//從重用池中取一個(gè)cell用來計(jì)算,必須以本方式從重用池中取,若以indexPath方式取由于-heightForRowAtIndexPath方法會(huì)造成循環(huán)科侈。
        cell.contentView.translatesAutoresizingMaskIntoConstraints = NO;//開啟約束
        cell.JustForCal = YES;//標(biāo)記只用來計(jì)算
        DicForTheUniqueCalCell[identifier] = cell;
    }
    //同上载佳,保證只有一個(gè)用來計(jì)算的cell
    return cell;
}

其實(shí)每一句注釋都表述的很清楚。不過老司機(jī)還是會(huì)一句一句給你說的臀栈,畢竟這才是老司機(jī)的風(fēng)格蔫慧,恩,就是墨跡权薯。

恩姑躲,老司機(jī)先說一說重用的問題吧。

我們都喜歡用tableView盟蚣,因?yàn)樗芎玫奶嫖覀冏隽藘?nèi)存控制的問題肋联。

他又是通過什么控制了內(nèi)存呢?節(jié)省了性能呢刁俭?通過重用橄仍。

這些大家都知道。但是有很多孩子誤會(huì)了重用啊牍戚,孩子你們不懂重用啊侮繁。

知道咋回事的這地方跳過吧。

前方高能預(yù)警如孝,以下內(nèi)容很基礎(chǔ)宪哩,真的很基礎(chǔ)真的真的很基礎(chǔ)第晰,只是給一些真的不知道的人看的锁孟。

當(dāng)一個(gè)cell將要離開屏幕時(shí),這個(gè)cell會(huì)進(jìn)入重用池茁瘦。重用池并不是什么特殊的東西品抽,就是系統(tǒng)給他放在一邊了。他只是單純的放在一邊了甜熔,不進(jìn)行任何操作圆恤。

當(dāng)一個(gè)cell將要進(jìn)入屏幕的時(shí)候,會(huì)調(diào)用tableView:(UITableView *)tableView cellForRowAtIndexPath這個(gè)代理腔稀,執(zhí)行其中的方法

說這兩句為了說明什么呢盆昙?
第一句我想說明的是,他只是放在重用池了焊虏,沒有進(jìn)行任淡喜!何!操诵闭!作炼团!

重點(diǎn)在哪呢?重點(diǎn)就在于存儲(chǔ)的是整個(gè)cell,包括cell原有的和你添加的所有子視圖们镜。

第二句我想說明的是币叹,他會(huì)執(zhí)行代理中的每!一模狭!句颈抚!話!

重點(diǎn)在哪呢嚼鹉?你從重用池中取出的cell贩汉,他是會(huì)對(duì)cell進(jìn)行再次進(jìn)行繪制

這兩句說明了什么锚赤?

  • cell上不要布置太多的控件匹舞,不然存入重用池也夠你吃一壺的。
  • tableView:(UITableView *)tableView cellForRowAtIndexPath這里面不要添加子視圖线脚,不然下次取出同一個(gè)cell的時(shí)候還會(huì)在同樣的位置再添加一個(gè)同樣的子視圖赐稽。你上下來回滑動(dòng)的時(shí)候cell就會(huì)添加無數(shù)個(gè)子視圖。
  • 沒有什么會(huì)影響重新繪制的浑侥,記住那句代碼一定會(huì)走姊舵,只要走就一定會(huì)繪制。如果說你繪制出了什么問題寓落,不要怪重用括丁,跟他沒關(guān)。問題一定在別的地方伶选。

恩史飞,這是老司機(jī)對(duì)重用的理解。

剛才跳過的旅客朋友們仰税,跳到這可以接上了构资。

接下來我們開始說這個(gè)方法。
為什么我們要取到這個(gè)cell呢肖卧?而不是隨便一個(gè)cell呢蚯窥?

因?yàn)槲覀儫o法保證或者指定只使用一種cell,很多情況下我們是自定義的cell塞帐。這樣的話每個(gè)不同種類的cell上的子視圖是不相同的,在自動(dòng)計(jì)算高度的時(shí)候?qū)ell的布局有很高要求巍沙,所以我們一定要保證我們計(jì)算用的cell與展示用的cell是同一種cell葵姥。

所以說我們這個(gè)方法只有一個(gè)參數(shù),identifier句携。因?yàn)樗菑闹赜贸刂腥〕鯿ell的唯一必要參數(shù)榔幸。

首先為了安全,先判斷傳入的identifier是否為空,若為空返回nil削咆。只是為了安全牍疏。還有代碼的嚴(yán)謹(jǐn)性。老司機(jī)又吹牛逼了拨齐,還代碼嚴(yán)謹(jǐn)性鳞陨,我的代碼通常都考慮不周全的。瞻惋。厦滤。

然后是通過runtime從綁定的屬性中取出一個(gè)字典。如果取到的這個(gè)字典為空則創(chuàng)建一個(gè)字典并綁定歼狼。

為什么要?jiǎng)?chuàng)建一個(gè)字典呢掏导,因?yàn)?code>我們要保證只取到這一個(gè)cell。這個(gè)cell是為了干什么的呢羽峰,就是為了計(jì)算高度的趟咆,那么我每次計(jì)算高度的時(shí)候只要有這么一個(gè)cell就好了,不要去初始化太多根本不用于顯示只用于計(jì)算的cell梅屉。

然后從字典中取出我們的cell值纱,如果取出的cell為空,則從重用池中取出一個(gè)cell履植。并存入字典计雌。

首先字典和cell的判空,都是針對(duì)第一次計(jì)算cell高度的時(shí)候來的玫霎。再次進(jìn)入的時(shí)候都不會(huì)為空凿滤。
必須要說明的是,從重用池中取出cell的方法我們一定要使用dequeueReusableCellWithIdentifier:這種方式去取庶近,不能以dequeueReusableCellWithIdentifier:indexPath那種方式去取翁脆。

就像老司機(jī)注釋中說的一樣,若以indexPath那種方式去取會(huì)造成雞生蛋蛋生雞的問題鼻种,你這程序就進(jìn)入死循環(huán)了反番。

然后cell.contentView.translatesAutoresizingMaskIntoConstraints = NO;這句是做什么呢?事實(shí)上叉钥,有兩種自動(dòng)布局方式罢缸,autoResizing和autoLayout。
autoResizing是UIView的固有屬性投队。是在IOS6之前用來實(shí)現(xiàn)自動(dòng)布局的屬性枫疆。當(dāng)然IOS6之后的autoLayout就要比他強(qiáng)大不少了。

事實(shí)上這個(gè)屬性默認(rèn)情況下是YES敷鸦。當(dāng)為YES時(shí)息楔,則我們設(shè)置約束是無效的寝贡。因?yàn)楹罄m(xù)我們要手動(dòng)添加一個(gè)約束輔助我們計(jì)算,所以這里我們將其設(shè)為NO值依。

然后將計(jì)算標(biāo)識(shí)符置真圃泡,標(biāo)識(shí)這個(gè)cell只參與計(jì)算高度,不負(fù)責(zé)展示愿险。以后遇到批量處理cell的時(shí)候可以判斷這個(gè)標(biāo)識(shí)符颇蜡,讓其不參與運(yùn)算。當(dāng)然老司機(jī)這里只是留了一個(gè)接口拯啦,實(shí)際我們有對(duì)其進(jìn)行處理澡匪。

通過這個(gè)方法,我們就成功的拿到了一個(gè)計(jì)算高度用的cell褒链。


-(CGFloat)DW_CalculateCellHeightWithCell:(UITableViewCell *)cell

第二個(gè)方法:

///根據(jù)cell計(jì)算cell的高度
-(CGFloat)DW_CalculateCellHeightWithCell:(UITableViewCell *)cell
{
    CGFloat width = self.bounds.size.width;
    //根據(jù)輔助視圖校正width
    if (cell.accessoryView) {
        width -= cell.accessoryView.bounds.size.width + 16;
    }
    else
    {
        static const CGFloat accessoryWidth[] = {
            [UITableViewCellAccessoryNone] = 0,
            [UITableViewCellAccessoryDisclosureIndicator] = 34,
            [UITableViewCellAccessoryDetailDisclosureButton] = 68,
            [UITableViewCellAccessoryCheckmark] = 40,
            [UITableViewCellAccessoryDetailButton] = 48
        };
        width -= accessoryWidth[cell.accessoryType];
    }
    CGFloat height = 0;
    if (!cell.NoAutoSizing && width > 0) {//如果不是非自適應(yīng)模式則添加約束后計(jì)算約束后高度
        NSLayoutConstraint * widthConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:width];//創(chuàng)建約束
        [cell.contentView addConstraint:widthConstraint];//添加約束
        height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;//計(jì)算高度
        [cell.contentView removeConstraint:widthConstraint];//移除約束
    }
    if (height == 0) {//如果約束錯(cuò)誤可能導(dǎo)致計(jì)算結(jié)果為零唁情,則以自適應(yīng)模式再次計(jì)算
        height = [cell sizeThatFits:CGSizeMake(width, 0)].height;
    }
    if (height == 0) {//如果計(jì)算仍然為0,則給出默認(rèn)高度
        height = 44;
    }
    if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {//如果不為無分割線模式則添加分割線高度
        height += 1.0 /[UIScreen mainScreen].scale;
    }
    return height;
}

首先我們要想計(jì)算出我們cell的高度甫匹,就需要拿到cell的contentView的實(shí)際寬度甸鸟。

1.先拿到cell的寬度

2.根據(jù)輔助視圖樣式校正寬度

如果有自定義輔助視圖,則按照自定義輔助視圖的寬度去校正兵迅,如果沒有按照系統(tǒng)輔助視圖樣式去校正寬度抢韭,然后根據(jù)寬度計(jì)算高度

3.然后如果是使用autoLayout進(jìn)行自適應(yīng)計(jì)算恍箭。

這個(gè)也是老司機(jī)為之后留下的接口刻恭,可以控制是否進(jìn)行一autoLayout進(jìn)行計(jì)算。但實(shí)際并沒有處理扯夭。

這里是添加約束的寫法鳍贾,先添加一個(gè)寬度約束,然后讓系統(tǒng)根據(jù)寬度約束自動(dòng)計(jì)算高度交洗,接著去掉我們添加的約束骑科。

4.如果根據(jù)約束計(jì)算結(jié)果錯(cuò)誤,則以sizeThatFits去計(jì)算高度构拳。

5.如果計(jì)算結(jié)果仍然為零咆爽,則給出默認(rèn)值44

6.判斷當(dāng)前tableView的分割線樣式置森,如果有分割線斗埂,還要校正高度

好了凫海,至此你已經(jīng)計(jì)算出這個(gè)cell應(yīng)該有的高度了蜜笤。


-(CGFloat)DW_CalCulateCellWithIdentifier:(NSString *)identifier configuration:(void(^)(id cell))configuration

最后一個(gè)方法:

///根據(jù)重用表示取出cell并操作cell后,計(jì)算高度
-(CGFloat)DW_CalCulateCellWithIdentifier:(NSString *)identifier
                           configuration:(void(^)(id cell))configuration
{
    if (!identifier.length) {
        return 0;
    }
    UITableViewCell * cell = [self DW_CalculateCellWithIdentifier:identifier];
    [cell prepareForReuse];//放回重用池
    if (configuration) {
        configuration(cell);//對(duì)cell進(jìn)行操作
   }
回重用池
    return [self DW_CalculateCellHeightWithCell:cell];
}

這個(gè)方法就比較簡(jiǎn)單了盐碱,先判斷重用標(biāo)示把兔。如果空,直接返回0瓮顽。還是只為了安全县好。

再通過第一個(gè)方法取出cell,然后將它放回重用池暖混,以至于下次我們還能取出來這個(gè)cell缕贡。

不要在意我之后還要對(duì)cell進(jìn)行操作,這個(gè)重用池只是一個(gè)概念拣播,其實(shí)并不是什么東西晾咪,只是標(biāo)志著這里面的cell可以用于重用,你完全可以理解成他只是cell的一個(gè)標(biāo)簽贮配。所以我之后還是可以繼續(xù)使用這個(gè)cell谍倦。不要糾結(jié)重用池、取出泪勒、放回了昼蛀,少年,他只是一個(gè)概念圆存。

哦對(duì)了叼旋,另外有一點(diǎn)你要注意,你記不記得老司機(jī)說過沦辙,進(jìn)入重用池夫植,是將整個(gè)cell存儲(chǔ)下來,并沒有做其他任何操作油讯。

其實(shí)你可以重寫prepareForReuse這個(gè)方法详民,這里可以做任何你想做的事。比如清除所有子視圖撞羽。不過有三點(diǎn)你需要注意:

  • 別忘了先寫[super prepareForReuse];
  • 真清除所有子視圖的時(shí)候記得別把contentView也刪了
  • 重寫之后上面的程序中你要合理的考慮一下[cell prepareForReuse]這句話的位置阐斜,反正這么跟你講,我是沒想出來放哪诀紊。谒出。

最后就是返回高度了。終于完事了邻奠。


留給外界的接口

-(CGFloat)DW_CalculateCellWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath configuration:(void(^)(id cell))configuration

我只說一個(gè)笤喳,這是最重要的一個(gè)返回高度的接口了,如果每次我們都計(jì)算高度碌宴,那我們這寫法也算是廢了杀狡,充其量算一個(gè)自動(dòng)返回高度的算法。

所以我們的邏輯應(yīng)該是先從cache里面中找贰镣,如果沒有呜象,計(jì)算并存儲(chǔ)膳凝。下次再找這個(gè)indexPath的時(shí)候就能找到了,正如下面的代碼一樣恭陡。

-(CGFloat)DW_CalculateCellWithIdentifier:(NSString *)identifier
                               indexPath:(NSIndexPath *)indexPath
                           configuration:(void(^)(id cell))configuration
{
    if(self.bounds.size.width != 0)//防止初始寬度為0(如autoLayout初次加載時(shí))
    {
        if (!identifier.length || !indexPath) {//非空判斷
            return 0;
        }
        NSString * key = [self.cache makeKeyWithIdentifier:identifier indexPath:indexPath];//制作key
        if ([self.cache existInCacheByKey:key]) {//如果key存在
            return [self.cache heightFromCacheWithKey:key];//從字典中取出高
        }
        CGFloat height = [self DW_CalCulateCellWithIdentifier:identifier configuration:configuration];//不存在則計(jì)算高度
        [self.cache cacheHeight:height byKey:key];//并緩存
        return height;
    }
    return 0;
}

最后關(guān)于如何使用

就是在原來返回tableView高度的方法出調(diào)用上面那個(gè)方法蹬音,僅此而已。

特別注意休玩,一定要在方法中先填充數(shù)據(jù)著淆,一定要在方法中先填充數(shù)據(jù),一定要在方法中先填充數(shù)據(jù)拴疤。重要的事情說三遍永部。否則你永遠(yuǎn)都是44啊親們。呐矾。

注入數(shù)據(jù)

我知道今天這個(gè)教程看上去很抽象苔埋,所以這次我會(huì)附上demo的鏈接。

demo地址

不過老司機(jī)還是想說一下自己對(duì)demo這件事的看法凫佛。

老司機(jī)能選擇在這里分享一些自己學(xué)到的東西讲坎,自然就不是一個(gè)敝帚自珍的人。然而之所以不愛附上demo鏈接是因?yàn)槔纤緳C(jī)覺得每次我都已經(jīng)很詳細(xì)的在博客中貼出我全部代碼而且一句一句講解真的已經(jīng)知無不言言無不盡了愧薛,我覺得編程這種東西還是得下手敲一遍晨炕,看別人的東西看一天也看不懂。所以我更提倡你們自己去敲一遍毫炉。如果我把demo鏈接一放出來瓮栗,你們直接下載了就去看,就去改瞄勾,真的沒有自己敲一遍學(xué)的快费奸。當(dāng)然有同學(xué)實(shí)在有需要可以留下郵箱,老司機(jī)會(huì)給你單獨(dú)發(fā)demo的进陡。


常用套話了愿阐,這么貪幕虛榮的老司機(jī)不就圖你點(diǎn)個(gè)喜歡么=。=趾疚,覺得好點(diǎn)個(gè)喜歡吧缨历。

轉(zhuǎn)載記得附上鏈接。
http://www.reibang.com/p/2b192257276f

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末糙麦,一起剝皮案震驚了整個(gè)濱河市辛孵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赡磅,老刑警劉巖魄缚,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異焚廊,居然都是意外死亡冶匹,警方通過查閱死者的電腦和手機(jī)习劫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來徙硅,“玉大人榜聂,你說我怎么就攤上這事∩つⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵匿乃,是天一觀的道長(zhǎng)桩皿。 經(jīng)常有香客問我,道長(zhǎng)幢炸,這世上最難降的妖魔是什么泄隔? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮宛徊,結(jié)果婚禮上佛嬉,老公的妹妹穿的比我還像新娘。我一直安慰自己闸天,他們只是感情好暖呕,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著苞氮,像睡著了一般湾揽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上笼吟,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天库物,我揣著相機(jī)與錄音,去河邊找鬼贷帮。 笑死戚揭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的撵枢。 我是一名探鬼主播民晒,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼诲侮!你這毒婦竟也來了镀虐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤沟绪,失蹤者是張志新(化名)和其女友劉穎刮便,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绽慈,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡恨旱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年辈毯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搜贤。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谆沃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出仪芒,到底是詐尸還是另有隱情唁影,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布掂名,位于F島的核電站据沈,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏饺蔑。R本人自食惡果不足惜锌介,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望猾警。 院中可真熱鬧孔祸,春花似錦、人聲如沸发皿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)雳窟。三九已至尊浪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間封救,已是汗流浹背拇涤。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留誉结,地道東北人鹅士。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像惩坑,于是被迫代替她去往敵國(guó)和親掉盅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

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

  • 前言 演示內(nèi)容:1.自適應(yīng)Cell2.瀑布流3.微信UI設(shè)計(jì)...... 1以舒、2演示內(nèi)容完成趾痘,后續(xù)再更新 參考資料...
    js丶閱讀 20,511評(píng)論 10 84
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件蔓钟、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,033評(píng)論 4 62
  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,125評(píng)論 29 470
  • 我們?cè)谏弦黄锻ㄟ^代碼自定義不等高cell》中學(xué)習(xí)了tableView的相關(guān)知識(shí)永票,本文將在上文的基礎(chǔ)上,利用sto...
    啊世ka閱讀 1,497評(píng)論 2 7
  • 我不要你陪我去到天涯海角 我只期盼像今日甜蜜不改變 還是喜歡你始終死心塌地的 每日回答都是愛你我的承諾 什么前塵往...
    花少顏閱讀 444評(píng)論 0 0