在許多關于 UITableview
性能優化的文章里都提到了緩存行高的優化方式,這也是蘋果工程師提出的改進建議.
正常情況下,heigtForRowAtIndexPath:
方法會被調用很多次,在 UITableview
滾動的過程中也會不斷的調用,這時如果我們只計算一次 Cell
的高度,之后每次調用時都返回緩存的高度,就能讓 UITableview
的滑動更加流暢,尤其是對高度計算特別耗時的復雜的 Cell 來說.
這篇文章中,我們來打造一個極簡的行高緩存工具類 ModelSizeCache
這樣命名是有原因的,我們來慢慢分析.
基本思路
下圖是我為了輔助說明緩存行高而制作的 Demo, 源碼在 Github, 建議結合源碼來看下面的博文
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 的內容變化了,比如上圖中的文字長度變化了,就要重新計算行高,并緩存起來.
由上面的說明我們得出以下結論:
- Cell 來計算高度最合適, Cell 知道自己的 View 是怎樣布局的,然后在傳入 Model ,就能計算出行高,所以我們在 Cell 中添加
-(CGFloat)calcCellHeightWithJoke:(Joke *)joke tableView:(UITableView *)tableView
方法來計算行高. - 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, 所以不能在后臺先執行計算,就將計算過程放在UITableview
的heightForRowAtIndexPath
方法,第一次請求該 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
性能優化的文章NSCache
Autolayout 計算行高