UITableView 行高自適應(yīng)+緩存優(yōu)化

學(xué)習(xí)了 FDTemplateLayoutCell 后斋日,我自己也寫(xiě)了一個(gè) TableView 行高自適應(yīng)加高度緩存的 Demo苞慢,本 Demo 研究實(shí)現(xiàn)了其中的最基本算高與緩存功能,僅供大家學(xué)習(xí)使用印机。

FDTemplateLayoutCell 原作博客

在開(kāi)始之前稀蟋,先讓我們了解一些 Runtime 的知識(shí)期吓,objc_setAssociatedObjectobjc_getAssociatedObject這兩個(gè)函數(shù)乘盖。

讓我們來(lái)看一個(gè)例子

/*
     object 要持有“別的對(duì)象”的對(duì)象
     key 關(guān)聯(lián)關(guān)鍵字焰檩,是一個(gè)字符串常量,是一個(gè)地址(這里注意订框,地址必須是不變的析苫,地址不同但是內(nèi)容相同的也不算同一個(gè)key)
     value 也就是值
     policy 這是一個(gè)枚舉,你可以點(diǎn)進(jìn)去看看這個(gè)枚舉是什么:
     OBJC_ASSOCIATION_ASSIGN
     OBJC_ASSOCIATION_RETAIN_NONATOMIC
     OBJC_ASSOCIATION_COPY_NONATOMIC
     OBJC_ASSOCIATION_RETAIN
     OBJC_ASSOCIATION_COPY
     */
    //參數(shù)一:需要添加屬性的對(duì)象 參數(shù)二:關(guān)聯(lián)關(guān)鍵字(關(guān)聯(lián)關(guān)鍵字要與get方法中的關(guān)鍵字相同穿扳,是一個(gè)指針類型) 參數(shù)三:屬性名 參數(shù)四:枚舉與@property括號(hào)中相同
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);

objc_setAssociatedObject這個(gè)函數(shù)的意思就是通過(guò)一個(gè) key 為一個(gè)對(duì)象綁定另一個(gè)對(duì)象

/*
     object 持有“別的對(duì)象”的對(duì)象衩侥,這里指a
     key 關(guān)聯(lián)關(guān)鍵字
     */
     objc_getAssociatedObject(self, @"name");

objc_getAssociatedObject這個(gè)函數(shù)的意思是通過(guò)一個(gè) key 取到一個(gè)對(duì)象綁定的那個(gè)對(duì)象

在上面這個(gè)例子中,我們使用這兩個(gè)函數(shù)矛物,為self所指的對(duì)象通過(guò)@"name"這個(gè) key 綁定了一個(gè)值為name的對(duì)象
Runtime 就說(shuō)這么多茫死,如果小伙伴們想要更為深入的了解,請(qǐng)自行搜尋相關(guān)資料履羞,至于為什么要說(shuō)這兩個(gè)函數(shù)峦萎,請(qǐng)小伙伴們繼續(xù)往下面看。

————前方高能預(yù)警————
下面就是本文的重點(diǎn)了

為 UITableViewCell 創(chuàng)建一個(gè) Category 目的是為其增加兩個(gè)屬性

為 Cell 添加兩個(gè)屬性忆首,一個(gè)用來(lái)標(biāo)志此 Cell 只用來(lái)計(jì)算高度爱榔,不進(jìn)行顯示,另一個(gè)屬性標(biāo)志是否使用約束來(lái)進(jìn)行計(jì)算糙及。添加這兩個(gè)屬性的目的是為了保證每一種類的 Cell 都有一個(gè)相應(yīng)的計(jì)算 Cell详幽,而且此種類的計(jì)算 Cell 有且只有一個(gè),如果你此時(shí)還有些懵逼浸锨,那請(qǐng)帶著你的疑問(wèn)繼續(xù)往下看妒潭。
什么?你說(shuō) Category 不能添加屬性揣钦?的確,Category 確實(shí)不能添加屬性漠酿,但是我們有萬(wàn)能的 Runtime 啊冯凹,來(lái)看看我們是怎么做的

@interface UITableViewCell (HeightCacheCell)

//添加兩個(gè)屬性
@property (assign, nonatomic)BOOL justForCalculate; //只用來(lái)計(jì)算的標(biāo)志

@property (assign, nonatomic)BOOL noAuotSizeing; //不依靠約束計(jì)算,只進(jìn)行自適應(yīng)

@end
@implementation UITableViewCell (HeightCacheCell)


#pragma mark ------ 綁定屬性

//justForCall
- (void)setJustForCalculate:(BOOL)justForCalculate{
    objc_setAssociatedObject(self, @selector(justForCalculate), @(justForCalculate), OBJC_ASSOCIATION_RETAIN); //使用get方法名作為key
}

- (BOOL)justForCalculate{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

//noAuotSizeing
- (void)setNoAuotSizeing:(BOOL)noAuotSizeing{
    objc_setAssociatedObject(self, @selector(noAuotSizeing), @(noAuotSizeing), OBJC_ASSOCIATION_RETAIN);
}

- (BOOL)noAuotSizeing{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

@end

重寫(xiě)這兩個(gè)屬性的 get set 方法炒嘲,并使用剛才學(xué)到的兩個(gè) Runtime 方法宇姚,為 UITableViewCell 綁定了兩個(gè)對(duì)象,這樣一來(lái)夫凸,我們就變相的為 UITableViewCell 添加了兩個(gè)屬性

創(chuàng)建一個(gè) Cache 類浑劳,用來(lái)緩存相應(yīng) Cell 的高度

@interface HeightCache : NSObject

@property (strong, nonatomic)NSMutableDictionary *heightCacheDicV; //豎直行高緩存字典
@property (strong, nonatomic)NSMutableDictionary *heightCacheDicH; //水平行高緩存字典
@property (strong, nonatomic)NSMutableDictionary *heightCacheDicCurrent; //當(dāng)前行高緩存字典

//制作key
- (NSString *)makeKeyWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath;

//判斷高度是否存在
- (BOOL)existInCacheByKey:(NSString *)key;

//查找高度緩存
- (CGFloat)heightFromCacheWithKey:(NSString *)key;

//緩存
- (void)cacheHieght:(CGFloat)hieght key:(NSString *)key;

@end

創(chuàng)建 HeightCache 這樣一個(gè)類,為其添加了三個(gè)字典作為屬性夭拌,分別存儲(chǔ)在手機(jī)橫屏豎屏下的 Cell 緩存高度魔熏,Current 字典為當(dāng)前手機(jī)屏幕狀態(tài)下的緩存字典衷咽,在它的懶加載方法中,我們將判斷使用的是橫屏緩存字典還是豎屏緩存字典蒜绽。暴露四個(gè)方法镶骗,分別是“制作從緩存字典中取緩存高度的 key”、“判斷此 key 下是否有緩存高度”躲雅、“通過(guò) key 取出緩存高度”鼎姊、“通過(guò) key 將對(duì)應(yīng)高度緩存”這四個(gè)方法。
實(shí)現(xiàn)相當(dāng)簡(jiǎn)單相赁,這里直接貼上代碼相寇,不做過(guò)多解釋。


@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.heightCacheDicCurrent objectForKey:key];
    return (value && ![value isEqualToNumber:@-1]);
}

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

//緩存
- (void)cacheHieght:(CGFloat)hieght key:(NSString *)key{
    [self.heightCacheDicCurrent setObject:@(hieght) forKey:key];
}

//lazy
- (NSMutableDictionary *)heightCacheDicH{
    if (!_heightCacheDicH) {
        _heightCacheDicH = [[NSMutableDictionary alloc] init];
    }
    return _heightCacheDicH;
}

- (NSMutableDictionary *)heightCacheDicV{
    if (!_heightCacheDicV) {
        _heightCacheDicV = [[NSMutableDictionary alloc] init];
    }
    return _heightCacheDicV;
}

//根據(jù)橫豎屏狀態(tài)選擇字典
- (NSMutableDictionary *)heightCacheDicCurrent{
    return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation)?self.heightCacheDicV:self.heightCacheDicH;
}

@end

重點(diǎn)钮科!創(chuàng)建 UITableView 的 Category 唤衫,計(jì)算 Cell 高度并緩存

我們首先為 UITableView 添加一個(gè) HeightCache 作為屬性,方便用來(lái)存儲(chǔ)高度緩存跺嗽,這里還是用 Runtime 的方法
#pragma mark ------ 綁定屬性

- (HeightCache *)heightCache{
    HeightCache *cache = objc_getAssociatedObject(self, _cmd);
    if (!cache) {
        cache = [[HeightCache alloc] init];
        objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return cache;
}

- (void)setHeightCache:(HeightCache *)heightCache{
    
    objc_setAssociatedObject(self, @selector(heightCache), heightCache, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
}
從復(fù)用池中獲取一個(gè)用于計(jì)算的 Cell
//獲取一個(gè)用于計(jì)算高度的Cell
- (__kindof UITableViewCell *)LLQ_CalculateCellWithIdentifier:(NSString *)identifier{
    
    if (!identifier.length) {
        return nil;
    }
    
    //runtime獲取一個(gè)存儲(chǔ)cell的字典
    NSMutableDictionary <NSString *, UITableViewCell *> *dicForTheUniqueCalCell = objc_getAssociatedObject(self, _cmd);
    //如果取不到战授,就綁定一個(gè)
    if (!dicForTheUniqueCalCell) {
        dicForTheUniqueCalCell = [[NSMutableDictionary alloc] init];
        objc_setAssociatedObject(self, _cmd, dicForTheUniqueCalCell, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    //取出cell,從綁定的字典取用
    UITableViewCell *cell = dicForTheUniqueCalCell[identifier];
    if (!cell) {
        cell = [self dequeueReusableCellWithIdentifier:identifier];
        cell.contentView.translatesAutoresizingMaskIntoConstraints = NO; //設(shè)置為NO才能用代碼使用AutoLayout
        cell.justForCalculate = YES; //設(shè)置只計(jì)算
        dicForTheUniqueCalCell[identifier] = cell;
    }
    
    return cell;
    
}

此方法中為 UITableView 綁定了一個(gè)字典桨嫁,目的是存儲(chǔ)某一種類的 Cell植兰,而區(qū)分 Cell 種類的辦法就是通過(guò) Cell 的重用標(biāo)識(shí)符。通過(guò)重用標(biāo)識(shí)符從字典中獲取 Cell璃吧,如果獲取不到楣导,就從 TableView 的復(fù)用池中取出一個(gè)此種類的 Cell,并設(shè)置只計(jì)算屬性畜挨,存入綁定的字典筒繁,這樣一來(lái),我們就保證了每種類的 Cell 有且只有一個(gè)用來(lái)計(jì)算巴元。要注意的是毡咏,在實(shí)際項(xiàng)目使用中我們必須使用-registerClass:forCellReuseIdentifier:-registerNib:forCellReuseIdentifier:其中之一的方法對(duì) Cell 進(jìn)行注冊(cè)。

計(jì)算 Cell 的高度
//計(jì)算cell高度
- (CGFloat)LLQ_CalculateCellHeightWithCell:(UITableViewCell *)cell{
    
    CGFloat width = self.bounds.size.width;
    
    //根據(jù)輔助視圖逮刨,調(diào)整寬度
    if (cell.accessoryView) {
        width -= cell.accessoryView.bounds.size.width + 16;
    }
    else{
        static const CGFloat accessoryWith[] = {
            [UITableViewCellAccessoryNone] = 0,
            [UITableViewCellAccessoryCheckmark] = 40,
            [UITableViewCellAccessoryDetailButton] = 48,
            [UITableViewCellAccessoryDisclosureIndicator] = 34,
            [UITableViewCellAccessoryDetailDisclosureButton] = 68,
        };
        width -= accessoryWith[cell.accessoryType];
    }
    
    CGFloat height = 0;
    
    //非自適應(yīng)模式呕缭,添加約束后計(jì)算約束后高度
    if (!cell.noAuotSizeing && width>0) {
        NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:width];
        [cell.contentView addConstraint:widthConstraint];
        //根據(jù)約束計(jì)算高度
        height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        [cell.contentView removeConstraint:widthConstraint]; //移除約束
    }
    
    //如果約束添加錯(cuò)誤,可能導(dǎo)致計(jì)算結(jié)果為0修己,則采用自適應(yīng)模式計(jì)算約束
    if (height == 0) {
        height = [cell sizeThatFits:CGSizeMake(width, 0)].height;
    }
    
    //還是為0恢总,默認(rèn)高度
    if (height == 0) {
        height = 44;
    }
    
    if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
        height += 1.0/[UIScreen mainScreen].scale;
    }
    
    return height;
}

首先計(jì)算 Cell 的 width,如果有輔助視圖睬愤,我們還要修正 width片仿,判斷是否是 AutoSizeing 模式,來(lái)決定使用哪種方式算 Cell 的高度尤辱,如果使用約束算高砂豌,就是通過(guò)添加一個(gè)我們算好的固定 width 的約束厢岂,從而得出 Cell 的高度。能夠這樣做的前提是我們?cè)?xib 中使用的 autolayout 約束正確奸鸯。在最后判斷一下有無(wú)分割線咪笑,做最后一次高度修正。

將上面兩個(gè)方法整合娄涩,給 Cell 填充數(shù)據(jù)后計(jì)算出當(dāng)前 Cell 的高度
//取出cell并對(duì)cell進(jìn)行操作窗怒,然后計(jì)算高度
- (CGFloat)LLQ_CalculateCellWithIdentifier:(NSString *)identifier configuration:(void(^)(id cell))configuration{
    
    if (!identifier.length) {
        return 0;
    }
    UITableViewCell *cell = [self LLQ_CalculateCellWithIdentifier:identifier];
    [cell prepareForReuse]; //放回重用池
    if (configuration) {
        configuration(cell);
    }
    
    return [self LLQ_CalculateCellHeightWithCell:cell];
    
}

首先獲取一個(gè) Cell 然后將其放回復(fù)用池(因?yàn)槲覀冊(cè)谌?Cell 的方法中沒(méi)有將其放回),然后給 Cell 填充數(shù)據(jù)蓄拣,這里使用了 block 將 Cell 傳遞到外界扬虚,填充完數(shù)據(jù)后使用算高方法計(jì)算高度。

計(jì)算高度球恤,并將計(jì)算的高度緩存辜昵,本方法暴露給外界共外界調(diào)用
//供外部調(diào)用的方法
- (CGFloat)LLQ_CalculateCellWithIdentifer:(NSString *)identifier indexPath:(NSIndexPath *)indexPath configuration:(void(^)(id cell))configuration{
    
    if (self.bounds.size.width != 0) {
        if (!identifier.length || !indexPath) {
            return 0;
        }
        NSString *key = [self.heightCache makeKeyWithIdentifier:identifier indexPath:indexPath];
        if ([self.heightCache existInCacheByKey:key]) {  //如果有緩存,就取出緩存
            return [self.heightCache heightFromCacheWithKey:key]; //從字典中取出高度
        }
        //沒(méi)有緩存咽斧,計(jì)算緩存
        CGFloat height = [self LLQ_CalculateCellWithIdentifier:identifier configuration:configuration];
        //并進(jìn)行緩存
        [self.heightCache cacheHieght:height key:key];
        return height;
    }
    
    return 0;
}

首先使用重用標(biāo)識(shí)符和 IndexPath 制作高度緩存的 key堪置,這樣制作出的 key 就能保證種類、組张惹、行的唯一性舀锨,然后使用這個(gè) key 去取緩存的高度,若沒(méi)有緩存高度就進(jìn)行計(jì)算宛逗。

本 Demo 實(shí)現(xiàn)了 TableView 的行高自適應(yīng)與行高緩存坎匿,這只是 FDTemplateLayoutCell 的一部分主要功能,在項(xiàng)目復(fù)雜情況下不夠適用雷激,比如在移動(dòng)一個(gè)單元格替蔬,刪除一個(gè)單元格等情況時(shí)本 Demo 沒(méi)有相應(yīng)的處理實(shí)現(xiàn),如果各位小伙伴項(xiàng)目需要屎暇,請(qǐng)直接使用 FDTemplateLayoutCell承桥。
本 Demo 僅供學(xué)習(xí)使用。

最后根悼,我還是會(huì)按照慣例把 Demo 共享給大家
Demo點(diǎn)這里凶异!點(diǎn)這里!點(diǎn)這里番挺!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市屯掖,隨后出現(xiàn)的幾起案子玄柏,更是在濱河造成了極大的恐慌,老刑警劉巖贴铜,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粪摘,死亡現(xiàn)場(chǎng)離奇詭異瀑晒,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)徘意,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)苔悦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人椎咧,你說(shuō)我怎么就攤上這事玖详。” “怎么了勤讽?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵蟋座,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我脚牍,道長(zhǎng)向臀,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任诸狭,我火速辦了婚禮券膀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘驯遇。我一直安慰自己芹彬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布妹懒。 她就那樣靜靜地躺著雀监,像睡著了一般。 火紅的嫁衣襯著肌膚如雪眨唬。 梳的紋絲不亂的頭發(fā)上会前,一...
    開(kāi)封第一講書(shū)人閱讀 51,165評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音匾竿,去河邊找鬼瓦宜。 笑死,一個(gè)胖子當(dāng)著我的面吹牛岭妖,可吹牛的內(nèi)容都是我干的临庇。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼昵慌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼假夺!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起斋攀,我...
    開(kāi)封第一講書(shū)人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤已卷,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后淳蔼,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體侧蘸,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡裁眯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了讳癌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片穿稳。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖晌坤,靈堂內(nèi)的尸體忽然破棺而出逢艘,到底是詐尸還是另有隱情,我是刑警寧澤泡仗,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布埋虹,位于F島的核電站,受9級(jí)特大地震影響娩怎,放射性物質(zhì)發(fā)生泄漏搔课。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一截亦、第九天 我趴在偏房一處隱蔽的房頂上張望爬泥。 院中可真熱鬧,春花似錦崩瓤、人聲如沸袍啡。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)境输。三九已至,卻和暖如春颖系,著一層夾襖步出監(jiān)牢的瞬間嗅剖,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工嘁扼, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留信粮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓趁啸,卻偏偏與公主長(zhǎng)得像强缘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子不傅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

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