iOS_緩存Cell行高的基本思路

在許多關(guān)于 UITableview 性能優(yōu)化的文章里都提到了緩存行高的優(yōu)化方式,這也是蘋(píng)果工程師提出的改進(jìn)建議.
正常情況下,heigtForRowAtIndexPath: 方法會(huì)被調(diào)用很多次,在 UITableview 滾動(dòng)的過(guò)程中也會(huì)不斷的調(diào)用,這時(shí)如果我們只計(jì)算一次 Cell的高度,之后每次調(diào)用時(shí)都返回緩存的高度,就能讓 UITableview 的滑動(dòng)更加流暢,尤其是對(duì)高度計(jì)算特別耗時(shí)的復(fù)雜的 Cell 來(lái)說(shuō).


這篇文章中,我們來(lái)打造一個(gè)極簡(jiǎn)的行高緩存工具類(lèi) ModelSizeCache
這樣命名是有原因的,我們來(lái)慢慢分析.

基本思路

下圖是我為了輔助說(shuō)明緩存行高而制作的 Demo, 源碼在 Github, 建議結(jié)合源碼來(lái)看下面的博文

demo.gif

cell 主要有3個(gè)控件

UIImageView *demoImageView;
UILabel *demoLabel;
UIStepper *demoStepper;

每一個(gè)展示一個(gè) Joke Model ,模型類(lèi)主要有4個(gè)屬性,

NSString *objectID;
NSString *content;
NSString *imageName;
NSInteger repeatCount; //文字內(nèi)容重復(fù)次數(shù),模擬 Model 中數(shù)據(jù)變化,重新計(jì)算高度的情況

ModelSizeCache 使用

相比沒(méi)有緩存高度的情況,只需修改一個(gè)方法:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    //先從緩存根據(jù) Model 的 hash 值取緩存的行高,如果沒(méi)有就調(diào)用后面的 orCalc Block計(jì)算行高,將計(jì)算結(jié)果存入緩存,然后返回行高.
    return [self.modelSizeCache getHeightForModel:self.jokes[indexPath.row] withTableView:tableView orCalc:^CGFloat(id model, UITableView *tableView) {
        return [self.prototypeCell calcCellHeightWithJoke:self.jokes[indexPath.row] tableView:tableView];
    }];
}

基本思路

  • 首先我們要計(jì)算一次 Cell 的高度,之后每次都返回緩存的高度
  • 我們的 Cell 的高度根據(jù) Model, 這里是 Joke 模型類(lèi)來(lái)計(jì)算的,所以我們緩存的高度應(yīng)該說(shuō)是填充完 Model 數(shù)據(jù)后 Cell的高度
  • 如果 Model 的內(nèi)容變化了,比如上圖中的文字長(zhǎng)度變化了,就要重新計(jì)算行高,并緩存起來(lái).

由上面的說(shuō)明我們得出以下結(jié)論:

  1. Cell 來(lái)計(jì)算高度最合適, Cell 知道自己的 View 是怎樣布局的,然后在傳入 Model ,就能計(jì)算出行高,所以我們?cè)?Cell 中添加
    -(CGFloat)calcCellHeightWithJoke:(Joke *)joke tableView:(UITableView *)tableView 方法來(lái)計(jì)算行高.
  2. Cell 的行高根據(jù)它填充的數(shù)據(jù)模型 Model 而計(jì)算出來(lái)的,所以我們要根據(jù) Model 來(lái)緩存行高.

第一點(diǎn): 計(jì)算 cell 高度

  • 本 Demo 為了簡(jiǎn)潔使用 AutoLayout + Storyboard布局,使用
    [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    來(lái)根據(jù) Cell 的約束來(lái)計(jì)算 Cell 高度

  • 如果你使用 [NSAttributedString boundingRectWithSize:options:context:] 來(lái)計(jì)算文字高度,在加上圖片的高度等的方式得出 Cell 高度,那么這個(gè) Cell 高度計(jì)算過(guò)程可以在從網(wǎng)絡(luò)加載完 JSON 數(shù)據(jù)就在后臺(tái)執(zhí)行,并將計(jì)算結(jié)果緩存起來(lái),在 UITableview請(qǐng)求 cell 高度時(shí),直接返回緩存的高度就好了,這樣就避免了在主線(xiàn)程計(jì)算 Cell 高度,達(dá)到了UITableview滑動(dòng)優(yōu)化目的.

  • 但由于我使用 [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; 方法來(lái)計(jì)算 Cell 高度,需要訪(fǎng)問(wèn) View, 所以不能在后臺(tái)先執(zhí)行計(jì)算,就將計(jì)算過(guò)程放在UITableviewheightForRowAtIndexPath方法,第一次請(qǐng)求該 Model 對(duì)應(yīng)的 Cell 行高時(shí)完成.

前文:在后臺(tái)計(jì)算 Model 對(duì)應(yīng)的行高思路來(lái)自于
YYKit 作者,ibireme iOS 保持界面流暢的技巧一文

預(yù)排版:
當(dāng)獲取到 API JSON 數(shù)據(jù)后,我會(huì)把每條 Cell 需要的數(shù)據(jù)都在后臺(tái)線(xiàn)程計(jì)算并封裝為一個(gè)布局對(duì)象 CellLayout。CellLayout 包含所有文本的 CoreText 排版結(jié)果、Cell 內(nèi)部每個(gè)控件的高度、Cell 的整體高度。每個(gè) CellLayout 的內(nèi)存占用并不多,所以當(dāng)生成后,可以全部緩存到內(nèi)存,以供稍后使用。這樣,TableView 在請(qǐng)求各個(gè)高度函數(shù)時(shí),不會(huì)消耗任何多余計(jì)算量;當(dāng)把 CellLayout 設(shè)置到 Cell 內(nèi)部時(shí),Cell 內(nèi)部也不用再計(jì)算布局了

對(duì)于通常的 TableView 來(lái)說(shuō),提前在后臺(tái)計(jì)算好布局結(jié)果是非常重要的一個(gè)性能優(yōu)化點(diǎn)。為了達(dá)到最高性能,你可能需要犧牲一些開(kāi)發(fā)速度,不要用 Autolayout 等技術(shù),少用 UILabel 等文本控件。但如果你對(duì)性能的要求并不那么高,可以嘗試用 TableView 的預(yù)估高度的功能,并把每個(gè) Cell 高度緩存下來(lái)。這里有個(gè)來(lái)自百度知道團(tuán)隊(duì)的開(kāi)源項(xiàng)目可以很方便的幫你實(shí)現(xiàn)這一點(diǎn):FDTemplateLayoutCell。

第二點(diǎn):緩存 Model 高度

我們將計(jì)算好的 Model 高度存入 NSCache 類(lèi)中,這個(gè)集合類(lèi)很像 NSMutableDictionary,主要有下面2個(gè)方法

- (nullable ObjectType)objectForKey:(KeyType)key;
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; 
  • 將高度存入 NSCache中需要一個(gè) Key, 一般我們的模型類(lèi),比如一條微博,一句聊天信息,都有一個(gè)唯一 ID, 我們可以使用它作為 Key
  • 但如果模型類(lèi)中的數(shù)據(jù)變化了,比如上面 Demo Gif 中的Joke模型類(lèi)的文字長(zhǎng)度變化了,就要讓這個(gè)緩存的高度失效,根據(jù) Model 的數(shù)據(jù)重新計(jì)算行高.

ModelSizeCache 具體實(shí)現(xiàn)

ModelSizeCache 定義了一個(gè) protocol

@protocol ModelSizeCacheProtocol <NSObject>

-(NSString*)modelID;

@end

任何需要緩存高度的模型類(lèi)都應(yīng)該遵守這個(gè)協(xié)議,返回 Model 的唯一 ID,這個(gè) ID 作為在 NSCache 中存取緩存行高的 Key.

ModelSizeCache 繼承自 NSObject, 有2個(gè)屬性

@property (strong,nonatomic) NSCache *cacheLandscape;
@property (strong,nonatomic) NSCache *cachePortrait;

分別緩存 Model 在橫豎屏狀態(tài)下的 Cell 高度,
主要的方法只有一個(gè)

-(CGFloat)getHeightForModel:(id<ModelSizeCacheProtocol>)model withTableView:(UITableView *)tableView orCalc:(CalcModelHeightBlock)block{
    //先從緩存中取行高
    CGSize modelSize= [self getCacheSizeForModel:model];
    //沒(méi)有就計(jì)算一下
    if( CGSizeEqualToSize(modelSize, NilCacheSize)){
        modelSize.height= block(model,tableView);
        
        //計(jì)算完成存入緩存中
        [self setOrientationSize:modelSize forModel:model];
        NSLog(@"計(jì)算行高 :%@",@(modelSize.height));
    }
    return modelSize.height;
}

其中 const CGSize NilCacheSize ={-1,-1};

然后使用時(shí)修改一個(gè)方法即可

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    //先從緩存根據(jù) Model 的 hash 值取緩存的行高,如果沒(méi)有就調(diào)用后面的 orCalc Block計(jì)算行高,將計(jì)算結(jié)果存入緩存,然后返回行高.
    return [self.modelSizeCache getHeightForModel:self.jokes[indexPath.row] withTableView:tableView orCalc:^CGFloat(id model, UITableView *tableView) {
        return [self.prototypeCell calcCellHeightWithJoke:self.jokes[indexPath.row] tableView:tableView];
    }];
}

最后在我們點(diǎn)擊 UIStepper 時(shí),更改模型類(lèi)的數(shù)據(jù),并讓緩存的高度失效即可,這樣會(huì)重新計(jì)算這個(gè) Model 的高度,并存入緩存,其它的 Model 直接讀取緩存,因?yàn)樗麄兊臄?shù)據(jù)沒(méi)有變化,Cell 的高度也就沒(méi)有變化.

-(void)cell:(Cell *)cell didStepperValueChanged:(NSInteger)value{
    NSIndexPath *indexPath= [self.tableView indexPathForCell:cell];
    Joke *joke= self.jokes[indexPath.row];
    joke.repeatCount=value; //修改 Model 的數(shù)據(jù)
    [self.modelSizeCache invalidateCacheForModel:joke]; //讓緩存失效
    [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}

這樣就達(dá)到了緩存行高,優(yōu)化UITableview 滑動(dòng)性能的作用.
完整代碼在 Github

關(guān)于用 ModelSizeCache存儲(chǔ)行高:

其實(shí)也可以用 Category + Associated Objects 為模型類(lèi)添加@property CGFloat height 屬性來(lái)存儲(chǔ)模型的高度,但是我覺(jué)得存儲(chǔ)在一個(gè)單獨(dú)的 ModelSizeCache中更合適,不污染模型類(lèi)的代碼,方便集中管理緩存數(shù)據(jù).

Ref

其它緩存行高的第三方庫(kù):

forkingdog/UITableView-FDTemplateLayoutCell
Raizlabs/RZCellSizeManager

UITableview 性能優(yōu)化的文章

iOS 保持界面流暢的技巧

NSCache

Autolayout 計(jì)算行高

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

相關(guān)閱讀更多精彩內(nèi)容

  • 概述在iOS開(kāi)發(fā)中UITableView可以說(shuō)是使用最廣泛的控件,我們平時(shí)使用的軟件中到處都可以看到它的影子,類(lèi)似...
    liudhkk閱讀 9,289評(píng)論 3 38
  • 我們?cè)谏弦黄锻ㄟ^(guò)代碼自定義不等高cell》中學(xué)習(xí)了tableView的相關(guān)知識(shí),本文將在上文的基礎(chǔ)上,利用sto...
    啊世ka閱讀 1,641評(píng)論 2 7
  • 今天在臺(tái)下看編劇別殺我、剛開(kāi)始看、我就覺(jué)得無(wú)聊。但是張浩和千惠第一次上臺(tái)、難免會(huì)出問(wèn)題。千惠剛開(kāi)始還是不錯(cuò)的、可是...
    愛(ài)吃糖的艾糖閱讀 298評(píng)論 0 0
  • 一個(gè)好友三個(gè)多月的時(shí)間里沒(méi)有再發(fā)朋友圈。時(shí)光匆忙,我也是在偶爾翻看微信好友的時(shí)候,才想起她。然后點(diǎn)進(jìn)她的頭像,發(fā)現(xiàn)...
    先森豬閱讀 431評(píng)論 0 0

友情鏈接更多精彩內(nèi)容