iOS_緩存Cell行高的基本思路

在許多關于 UITableview 性能優化的文章里都提到了緩存行高的優化方式,這也是蘋果工程師提出的改進建議.
正常情況下,heigtForRowAtIndexPath: 方法會被調用很多次,在 UITableview 滾動的過程中也會不斷的調用,這時如果我們只計算一次 Cell的高度,之后每次調用時都返回緩存的高度,就能讓 UITableview 的滑動更加流暢,尤其是對高度計算特別耗時的復雜的 Cell 來說.


這篇文章中,我們來打造一個極簡的行高緩存工具類 ModelSizeCache
這樣命名是有原因的,我們來慢慢分析.

基本思路

下圖是我為了輔助說明緩存行高而制作的 Demo, 源碼在 Github, 建議結合源碼來看下面的博文

demo.gif

cell 主要有3個控件

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

每一個展示一個 Joke Model ,模型類主要有4個屬性,

NSString *objectID;
NSString *content;
NSString *imageName;
NSInteger repeatCount; //文字內容重復次數,模擬 Model 中數據變化,重新計算高度的情況

ModelSizeCache 使用

相比沒有緩存高度的情況,只需修改一個方法:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    //先從緩存根據 Model 的 hash 值取緩存的行高,如果沒有就調用后面的 orCalc Block計算行高,將計算結果存入緩存,然后返回行高.
    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];
    }];
}

基本思路

  • 首先我們要計算一次 Cell 的高度,之后每次都返回緩存的高度
  • 我們的 Cell 的高度根據 Model, 這里是 Joke 模型類來計算的,所以我們緩存的高度應該說是填充完 Model 數據后 Cell的高度
  • 如果 Model 的內容變化了,比如上圖中的文字長度變化了,就要重新計算行高,并緩存起來.

由上面的說明我們得出以下結論:

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

第一點: 計算 cell 高度

  • 本 Demo 為了簡潔使用 AutoLayout + Storyboard布局,使用
    [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    來根據 Cell 的約束來計算 Cell 高度

  • 如果你使用 [NSAttributedString boundingRectWithSize:options:context:] 來計算文字高度,在加上圖片的高度等的方式得出 Cell 高度,那么這個 Cell 高度計算過程可以在從網絡加載完 JSON 數據就在后臺執行,并將計算結果緩存起來,在 UITableview請求 cell 高度時,直接返回緩存的高度就好了,這樣就避免了在主線程計算 Cell 高度,達到了UITableview滑動優化目的.

  • 但由于我使用 [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; 方法來計算 Cell 高度,需要訪問 View, 所以不能在后臺先執行計算,就將計算過程放在UITableviewheightForRowAtIndexPath方法,第一次請求該 Model 對應的 Cell 行高時完成.

前文:在后臺計算 Model 對應的行高思路來自于
YYKit 作者,ibireme iOS 保持界面流暢的技巧一文

預排版:
當獲取到 API JSON 數據后,我會把每條 Cell 需要的數據都在后臺線程計算并封裝為一個布局對象 CellLayout。CellLayout 包含所有文本的 CoreText 排版結果、Cell 內部每個控件的高度、Cell 的整體高度。每個 CellLayout 的內存占用并不多,所以當生成后,可以全部緩存到內存,以供稍后使用。這樣,TableView 在請求各個高度函數時,不會消耗任何多余計算量;當把 CellLayout 設置到 Cell 內部時,Cell 內部也不用再計算布局了

對于通常的 TableView 來說,提前在后臺計算好布局結果是非常重要的一個性能優化點。為了達到最高性能,你可能需要犧牲一些開發速度,不要用 Autolayout 等技術,少用 UILabel 等文本控件。但如果你對性能的要求并不那么高,可以嘗試用 TableView 的預估高度的功能,并把每個 Cell 高度緩存下來。這里有個來自百度知道團隊的開源項目可以很方便的幫你實現這一點:FDTemplateLayoutCell

第二點:緩存 Model 高度

我們將計算好的 Model 高度存入 NSCache 類中,這個集合類很像 NSMutableDictionary,主要有下面2個方法

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

ModelSizeCache 具體實現

ModelSizeCache 定義了一個 protocol

@protocol ModelSizeCacheProtocol <NSObject>

-(NSString*)modelID;

@end

任何需要緩存高度的模型類都應該遵守這個協議,返回 Model 的唯一 ID,這個 ID 作為在 NSCache 中存取緩存行高的 Key.

ModelSizeCache 繼承自 NSObject, 有2個屬性

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

分別緩存 Model 在橫豎屏狀態下的 Cell 高度,
主要的方法只有一個

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

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

然后使用時修改一個方法即可

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    //先從緩存根據 Model 的 hash 值取緩存的行高,如果沒有就調用后面的 orCalc Block計算行高,將計算結果存入緩存,然后返回行高.
    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];
    }];
}

最后在我們點擊 UIStepper 時,更改模型類的數據,并讓緩存的高度失效即可,這樣會重新計算這個 Model 的高度,并存入緩存,其它的 Model 直接讀取緩存,因為他們的數據沒有變化,Cell 的高度也就沒有變化.

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

這樣就達到了緩存行高,優化UITableview 滑動性能的作用.
完整代碼在 Github

關于用 ModelSizeCache存儲行高:

其實也可以用 Category + Associated Objects 為模型類添加@property CGFloat height 屬性來存儲模型的高度,但是我覺得存儲在一個單獨的 ModelSizeCache中更合適,不污染模型類的代碼,方便集中管理緩存數據.

Ref

其它緩存行高的第三方庫:

forkingdog/UITableView-FDTemplateLayoutCell
Raizlabs/RZCellSizeManager

UITableview 性能優化的文章

iOS 保持界面流暢的技巧

NSCache

Autolayout 計算行高

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

推薦閱讀更多精彩內容