在許多關(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)看下面的博文

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é)論:
- Cell 來(lái)計(jì)算高度最合適, Cell 知道自己的 View 是怎樣布局的,然后在傳入 Model ,就能計(jì)算出行高,所以我們?cè)?Cell 中添加
-(CGFloat)calcCellHeightWithJoke:(Joke *)joke tableView:(UITableView *)tableView方法來(lái)計(jì)算行高. - 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ò)程放在UITableview的heightForRowAtIndexPath方法,第一次請(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)化的文章NSCache
Autolayout 計(jì)算行高