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

學習了 FDTemplateLayoutCell 后,我自己也寫了一個 TableView 行高自適應加高度緩存的 Demo,本 Demo 研究實現了其中的最基本算高與緩存功能,僅供大家學習使用。

FDTemplateLayoutCell 原作博客

在開始之前,先讓我們了解一些 Runtime 的知識,objc_setAssociatedObjectobjc_getAssociatedObject這兩個函數。

讓我們來看一個例子

/*
     object 要持有“別的對象”的對象
     key 關聯關鍵字,是一個字符串常量,是一個地址(這里注意,地址必須是不變的,地址不同但是內容相同的也不算同一個key)
     value 也就是值
     policy 這是一個枚舉,你可以點進去看看這個枚舉是什么:
     OBJC_ASSOCIATION_ASSIGN
     OBJC_ASSOCIATION_RETAIN_NONATOMIC
     OBJC_ASSOCIATION_COPY_NONATOMIC
     OBJC_ASSOCIATION_RETAIN
     OBJC_ASSOCIATION_COPY
     */
    //參數一:需要添加屬性的對象 參數二:關聯關鍵字(關聯關鍵字要與get方法中的關鍵字相同,是一個指針類型) 參數三:屬性名 參數四:枚舉與@property括號中相同
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);

objc_setAssociatedObject這個函數的意思就是通過一個 key 為一個對象綁定另一個對象

/*
     object 持有“別的對象”的對象,這里指a
     key 關聯關鍵字
     */
     objc_getAssociatedObject(self, @"name");

objc_getAssociatedObject這個函數的意思是通過一個 key 取到一個對象綁定的那個對象

在上面這個例子中,我們使用這兩個函數,為self所指的對象通過@"name"這個 key 綁定了一個值為name的對象
Runtime 就說這么多,如果小伙伴們想要更為深入的了解,請自行搜尋相關資料,至于為什么要說這兩個函數,請小伙伴們繼續(xù)往下面看。

————前方高能預警————
下面就是本文的重點了

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

為 Cell 添加兩個屬性,一個用來標志此 Cell 只用來計算高度,不進行顯示,另一個屬性標志是否使用約束來進行計算。添加這兩個屬性的目的是為了保證每一種類的 Cell 都有一個相應的計算 Cell,而且此種類的計算 Cell 有且只有一個,如果你此時還有些懵逼,那請帶著你的疑問繼續(xù)往下看。
什么?你說 Category 不能添加屬性?的確,Category 確實不能添加屬性,但是我們有萬能的 Runtime 啊,來看看我們是怎么做的

@interface UITableViewCell (HeightCacheCell)

//添加兩個屬性
@property (assign, nonatomic)BOOL justForCalculate; //只用來計算的標志

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

@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

重寫這兩個屬性的 get set 方法,并使用剛才學到的兩個 Runtime 方法,為 UITableViewCell 綁定了兩個對象,這樣一來,我們就變相的為 UITableViewCell 添加了兩個屬性

創(chuàng)建一個 Cache 類,用來緩存相應 Cell 的高度

@interface HeightCache : NSObject

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

//制作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 這樣一個類,為其添加了三個字典作為屬性,分別存儲在手機橫屏豎屏下的 Cell 緩存高度,Current 字典為當前手機屏幕狀態(tài)下的緩存字典,在它的懶加載方法中,我們將判斷使用的是橫屏緩存字典還是豎屏緩存字典。暴露四個方法,分別是“制作從緩存字典中取緩存高度的 key”、“判斷此 key 下是否有緩存高度”、“通過 key 取出緩存高度”、“通過 key 將對應高度緩存”這四個方法。
實現相當簡單,這里直接貼上代碼,不做過多解釋。


@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;
}

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

@end

重點!創(chuàng)建 UITableView 的 Category ,計算 Cell 高度并緩存

我們首先為 UITableView 添加一個 HeightCache 作為屬性,方便用來存儲高度緩存,這里還是用 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);
    
}
從復用池中獲取一個用于計算的 Cell
//獲取一個用于計算高度的Cell
- (__kindof UITableViewCell *)LLQ_CalculateCellWithIdentifier:(NSString *)identifier{
    
    if (!identifier.length) {
        return nil;
    }
    
    //runtime獲取一個存儲cell的字典
    NSMutableDictionary <NSString *, UITableViewCell *> *dicForTheUniqueCalCell = objc_getAssociatedObject(self, _cmd);
    //如果取不到,就綁定一個
    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; //設置為NO才能用代碼使用AutoLayout
        cell.justForCalculate = YES; //設置只計算
        dicForTheUniqueCalCell[identifier] = cell;
    }
    
    return cell;
    
}

此方法中為 UITableView 綁定了一個字典,目的是存儲某一種類的 Cell,而區(qū)分 Cell 種類的辦法就是通過 Cell 的重用標識符。通過重用標識符從字典中獲取 Cell,如果獲取不到,就從 TableView 的復用池中取出一個此種類的 Cell,并設置只計算屬性,存入綁定的字典,這樣一來,我們就保證了每種類的 Cell 有且只有一個用來計算。要注意的是,在實際項目使用中我們必須使用-registerClass:forCellReuseIdentifier:-registerNib:forCellReuseIdentifier:其中之一的方法對 Cell 進行注冊。

計算 Cell 的高度
//計算cell高度
- (CGFloat)LLQ_CalculateCellHeightWithCell:(UITableViewCell *)cell{
    
    CGFloat width = self.bounds.size.width;
    
    //根據輔助視圖,調整寬度
    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;
    
    //非自適應模式,添加約束后計算約束后高度
    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];
        //根據約束計算高度
        height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        [cell.contentView removeConstraint:widthConstraint]; //移除約束
    }
    
    //如果約束添加錯誤,可能導致計算結果為0,則采用自適應模式計算約束
    if (height == 0) {
        height = [cell sizeThatFits:CGSizeMake(width, 0)].height;
    }
    
    //還是為0,默認高度
    if (height == 0) {
        height = 44;
    }
    
    if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
        height += 1.0/[UIScreen mainScreen].scale;
    }
    
    return height;
}

首先計算 Cell 的 width,如果有輔助視圖,我們還要修正 width,判斷是否是 AutoSizeing 模式,來決定使用哪種方式算 Cell 的高度,如果使用約束算高,就是通過添加一個我們算好的固定 width 的約束,從而得出 Cell 的高度。能夠這樣做的前提是我們在 xib 中使用的 autolayout 約束正確。在最后判斷一下有無分割線,做最后一次高度修正。

將上面兩個方法整合,給 Cell 填充數據后計算出當前 Cell 的高度
//取出cell并對cell進行操作,然后計算高度
- (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];
    
}

首先獲取一個 Cell 然后將其放回復用池(因為我們在取 Cell 的方法中沒有將其放回),然后給 Cell 填充數據,這里使用了 block 將 Cell 傳遞到外界,填充完數據后使用算高方法計算高度。

計算高度,并將計算的高度緩存,本方法暴露給外界共外界調用
//供外部調用的方法
- (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]; //從字典中取出高度
        }
        //沒有緩存,計算緩存
        CGFloat height = [self LLQ_CalculateCellWithIdentifier:identifier configuration:configuration];
        //并進行緩存
        [self.heightCache cacheHieght:height key:key];
        return height;
    }
    
    return 0;
}

首先使用重用標識符和 IndexPath 制作高度緩存的 key,這樣制作出的 key 就能保證種類、組、行的唯一性,然后使用這個 key 去取緩存的高度,若沒有緩存高度就進行計算。

本 Demo 實現了 TableView 的行高自適應與行高緩存,這只是 FDTemplateLayoutCell 的一部分主要功能,在項目復雜情況下不夠適用,比如在移動一個單元格,刪除一個單元格等情況時本 Demo 沒有相應的處理實現,如果各位小伙伴項目需要,請直接使用 FDTemplateLayoutCell
本 Demo 僅供學習使用。

最后,我還是會按照慣例把 Demo 共享給大家
Demo點這里!點這里!點這里!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容