前言
對于TableViewCell的高度自適應(yīng),很多初次接觸的同學(xué)簿晓,還是很頭痛的眶拉。就算已經(jīng)有些開發(fā)經(jīng)驗(yàn)的同學(xué),處理起來也可能用錯(cuò)了方法憔儿。但其實(shí)系統(tǒng)已經(jīng)提供了很方便的處理方法忆植,我們這里就系統(tǒng)的高度計(jì)算做一個(gè)講解。然后主要要講的谒臼,是我在實(shí)際開發(fā)中(我們App加入了直播功能朝刊,直播中要處理大量的聊天消息)用到的方法,也是在性能上優(yōu)化了很多的方法蜈缤,將計(jì)算好的高度緩存下來拾氓,在大量數(shù)據(jù)(幾百、幾千條數(shù)據(jù))進(jìn)行刷新底哥、插入數(shù)據(jù)咙鞍、刪除數(shù)據(jù)等操作的時(shí)候也能保證性能、流暢性趾徽,而相比于其他高度緩存方案续滋,這種方式的高度緩存,更方便管理孵奶。以下高度都結(jié)合Masonry來完成(畢竟手寫Autolayout還是Masonry比較方便)疲酌,使用XIB的同學(xué),也可以直接拖約束了袁。
場景模擬
我們寫個(gè)Demo朗恳,來模擬下直播聊天室中情況,眾所周知早像,直播聊天室中的消息量是巨大的僻肖,而且刷新特別快,在刷新聊天列表的時(shí)候卢鹦,最耗費(fèi)性能的就是UITableView的兩個(gè)代理方法臀脏,一個(gè)heightForRowAtIndexPath,一個(gè)cellForRowAtIndexPath冀自。無論是刷新還是新增揉稚、刪除,都會反復(fù)觸發(fā)這兩個(gè)方法熬粗,而對于聊天室搀玖,如果從后面追加數(shù)據(jù),假設(shè)你原來有1000條數(shù)據(jù)驻呐,即使你從后面insert一個(gè)cell灌诅,那也會調(diào)用1000次HeightForRow芳来,如果你在計(jì)算高度的時(shí)候,使用了很復(fù)雜的計(jì)算方式猜拾,就很影響性能了即舌。
首先新建個(gè)項(xiàng)目,然后在項(xiàng)目中加入Masonry挎袜,再然后加入一個(gè)顯示當(dāng)前屏幕FPS的label進(jìn)來顽聂,提取自YYKit,YYFPSLabel盯仪。這樣就能大致了解性能如何了紊搪。然后我們在ViewController.m中加入這個(gè)控件:
- (void)viewDidLoad {
[super viewDidLoad];
YYFPSLabel *fpsLabel = [[YYFPSLabel alloc] initWithFrame:CGRectMake(0, 20, 60, 20)];
[self.view addSubview:fpsLabel];
}
運(yùn)行后我們的Demo頂部就會顯示FPS了:
然后我們先建一個(gè)Model,和一個(gè)Cell全景,Model代表我們從服務(wù)器請求的數(shù)據(jù)模型耀石,Cell就是我們要用到的展示內(nèi)容的Cell。為了讓Cell更符合實(shí)際項(xiàng)目的需求蚪燕,我們讓cell顯示多一些的內(nèi)容娶牌,來一個(gè)拼接的屬性字符串吧奔浅。
新建個(gè)Model:
模擬聊天中的消息展示馆纳,我們給Model兩個(gè)屬性,一個(gè)姓名汹桦,一個(gè)發(fā)言內(nèi)容:
// 姓名
@property (nonatomic, copy) NSString *name;
// 發(fā)言內(nèi)容
@property (nonatomic, copy) NSString *message;
我們再新建一個(gè)Cell鲁驶,在Cell中將內(nèi)容展示出來:
我們的Cell中只有一個(gè)Label,用于展示“姓名:發(fā)言內(nèi)容”這樣的內(nèi)容舞骆,注意這里布局钥弯,采用自動布局,Cell的ContentView由Label中的內(nèi)容撐開:
@interface MessageCell ()
@property (nonatomic, strong) UILabel *messsageLabel;
@end
@implementation MessageCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self == [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
// 創(chuàng)建UI
[self createUI];
}
return self;
}
- (void)createUI {
/** 發(fā)言 */
self.messsageLabel = [[UILabel alloc] init];
self.messsageLabel.numberOfLines = 0;
[self.contentView addSubview:self.messsageLabel];
[self.messsageLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(8);
make.left.mas_equalTo(10);
make.right.mas_equalTo(-10);
make.bottom.mas_equalTo(-8);
}];
}
- (void)setMessage:(CellModel *)message {
// 創(chuàng)建一個(gè)可變屬性字符串
NSMutableAttributedString *finalStr = [[NSMutableAttributedString alloc] init];
// 創(chuàng)建姓名
NSAttributedString *nameStr = [[NSAttributedString alloc] initWithString:message.name attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor redColor]}];
// 創(chuàng)建發(fā)言內(nèi)容
NSAttributedString *messageStr = [[NSAttributedString alloc] initWithString:message.message attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor blackColor]}];
// 拼接上兩個(gè)字符串
[finalStr appendAttributedString:nameStr];
[finalStr appendAttributedString:messageStr];
self.messsageLabel.attributedText = finalStr;
}
@end
這里我們需要注意的是督禽,Label要高度自適應(yīng)的撐開Cell的ContentView的高度脆霎。然后我們?nèi)iewController中添加一個(gè)用于展示這些內(nèi)容的TableView,在viewDidLoad方法的結(jié)尾狈惫,我們添加一個(gè)按鈕睛蛛,該按鈕模擬聊天室中接收到了新消息,并滾動到TableView的最底部胧谈。具體代碼如下:
@interface ViewController () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSMutableArray *dataArr;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
YYFPSLabel *fpsLabel = [[YYFPSLabel alloc] initWithFrame:CGRectMake(0, 20, 60, 20)];
[self.view addSubview:fpsLabel];
// 創(chuàng)建TableView
self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.height-100) style:0];
self.tableView.dataSource = self;
self.tableView.delegate = self;
[self.view addSubview:self.tableView];
// 注冊cell
[self.tableView registerClass:[MessageCell class] forCellReuseIdentifier:@"MessageCell"];
// 模擬一些數(shù)據(jù)源
NSArray *nameArr = @[@"張三:",
@"李四:",
@"王五:",
@"陳六:",
@"吳老二:"];
NSArray *messageArr = @[@"ash快點(diǎn)回家愛是妒忌哈市黨和國家按時(shí)到崗哈時(shí)代光華撒國會大廈國會大廈國會大廈更好的噶山東黃金撒旦哈安師大噶是個(gè)混蛋撒",
@"傲世江湖點(diǎn)撒恭候大駕水草瑪瑙現(xiàn)在才明白你個(gè)壞蛋擦邊沙塵暴你先走吧出現(xiàn)在",
@"撒點(diǎn)花噶閃光燈",
@"按時(shí)間大公司大概好久撒大概好久撒黨和國家按時(shí)到崗哈師大就薩達(dá)數(shù)據(jù)庫化打算幾點(diǎn)撒謊就看電視驕傲的撒金葵花打暑假工大撒比的撒謊講大話手機(jī)巴士差距啊市場報(bào)價(jià)啊山東黃金as擦傷擦啊as擦肩時(shí)擦市場報(bào)價(jià)按時(shí)VC阿擦把持啊三重才撒啊雙層巴士吃按時(shí)吃啊雙層巴士擦報(bào)啥錯(cuò)",
@"as大帥哥大孤山街道安師大好噶?xí)r間過得撒黃金國度"];
// 向數(shù)據(jù)源中隨機(jī)放入500個(gè)Model
self.dataArr = [[NSMutableArray alloc] init];
for (int i=0; i<500; i++) {
CellModel *model = [[CellModel alloc] init];
model.name = nameArr[arc4random()%nameArr.count];
model.message = messageArr[arc4random()%messageArr.count];
[self.dataArr addObject:model];
}
// 我們再創(chuàng)建一個(gè)按鈕忆肾,點(diǎn)擊可從后面追加一些數(shù)據(jù)進(jìn)來
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 40, 100, 60)];
button.backgroundColor = [UIColor redColor];
[self.view addSubview:button];
[button addTarget:self action:@selector(addData) forControlEvents:UIControlEventTouchUpInside];
}
- (void)addData {
// 添加一個(gè)Model,在追加到Tableview中
CellModel *model = [[CellModel alloc] init];
model.name = @"皮皮:";
model.message = @"安師大公司的嘎斯大時(shí)代安師大嘎斯高大上撒旦嘎嘎就是打閃光燈";
[self.dataArr addObject:model];
// 插入到tableView中
[self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:self.dataArr.count-1 inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
// 再滾動到最底部
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.dataArr.count-1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataArr.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 44;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MessageCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MessageCell" forIndexPath:indexPath];
[cell setMessage:self.dataArr[indexPath.row]];
return cell;
}
@end
效果如下菱肖,這里我們固定Cell高度為44了客冈,所以全程怎么滾動,F(xiàn)PS都是60:
動態(tài)高度一:系統(tǒng)自帶支持
那好了稳强,上面的固定高度測試完了场仲,我們來測試下適配Cell高度的方法和悦。首先采用系統(tǒng)的動態(tài)高度方法。
我們需要做兩件事:第一:指定TableView的高度為自適應(yīng):
// 必須設(shè)置預(yù)估高度才能生效
self.tableView.estimatedRowHeight = 100;
self.tableView.rowHeight = UITableViewAutomaticDimension;
第二:將TableView的行高代理方法注釋掉渠缕,也就是下面這個(gè)方法:
//- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// return 44;
//}
這時(shí)再運(yùn)行摹闽,你會發(fā)現(xiàn),Cell的高度已經(jīng)自動適配褐健,滾動中也特別流暢付鹿,保持60幀:
但如果點(diǎn)擊我們的紅色按鈕,就卡爆了蚜迅,而且會有一個(gè)刷新的白屏:
實(shí)測系統(tǒng)的這個(gè)方法舵匾,只適用于iOS8及以上,且在數(shù)據(jù)量超大的時(shí)候谁不,進(jìn)行插入和刪除坐梯,都是很不流暢的,不建議采用刹帕。當(dāng)然這種方法針對一些常用場景吵血,比如新聞列表、商品列表什么的偷溺,數(shù)據(jù)量沒那么大且不涉及到新增蹋辅、刪除數(shù)據(jù)的時(shí)候,這種方法挫掏,還是蠻不錯(cuò)的侦另,寫起來很簡便。
動態(tài)高度二:自己計(jì)算高度
我們將上面的方法撤回尉共,試驗(yàn)下自己計(jì)算Cell高度褒傅,性能如何。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// 創(chuàng)建一個(gè)可變屬性字符串
NSMutableAttributedString *finalStr = [[NSMutableAttributedString alloc] init];
// 取出Model
CellModel *message = self.dataArr[indexPath.row];
// 創(chuàng)建姓名
NSAttributedString *nameStr = [[NSAttributedString alloc] initWithString:message.name attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor redColor]}];
// 創(chuàng)建發(fā)言內(nèi)容
NSAttributedString *messageStr = [[NSAttributedString alloc] initWithString:message.message attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor blackColor]}];
// 拼接上兩個(gè)字符串
[finalStr appendAttributedString:nameStr];
[finalStr appendAttributedString:messageStr];
// 計(jì)算高度
CGSize size = [finalStr boundingRectWithSize:CGSizeMake(self.view.frame.size.width-20, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin context:nil].size;
return ceil(size.height);
}
這種方式袄友,在滾動列表的時(shí)候殿托,還是60幀流暢的,點(diǎn)擊紅色按鈕后剧蚣,會降到47幀支竹,并持續(xù)一小段時(shí)間,所以這段時(shí)間中券敌,你如果是在聊天室中播放彈幕唾戚,或者進(jìn)行點(diǎn)贊動畫的處理的時(shí)候,這些內(nèi)容都會卡住待诅,直到這段時(shí)間過去叹坦,當(dāng)然相比于系統(tǒng)的方法,性能還是稍好一點(diǎn)的:
動態(tài)高度三:Autolayout計(jì)算高度
有人可能覺得卑雁,上面計(jì)算高度太麻煩了募书,不就是把Cell中setMessage拿出來再寫一遍嘛绪囱,同樣的代碼不要寫兩次,那我們換種方式來寫莹捡。這里我們先給ViewController這個(gè)Controller加一個(gè)屬性鬼吵,下面的這個(gè)Cell,承擔(dān)了計(jì)算Cell高度的工作:
@property (nonatomic, strong) MessageCell *tempCell;
在viewDidLoad中初始化:
self.tempCell = [[MessageCell alloc] initWithStyle:0 reuseIdentifier:@"MessageCell"];
然后我們給Cell加個(gè)方法篮赢,這里需要注意的是齿椅,我們要對最終算出來的高度加1,這個(gè)1是Cell的分割線的高度启泣,當(dāng)前如果你隱藏了分割線涣脚,就不需要加這個(gè)1了:
// 根絕數(shù)據(jù)計(jì)算cell的高度
- (CGFloat)heightForModel:(CellModel *)message {
[self setMessage:message];
[self layoutIfNeeded];
CGFloat cellHeight = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height+1;
return cellHeight;
}
還要指定Cell中的Label的最大寬度,保證在適配Label的時(shí)候寥茫,不會超出這個(gè)寬度:
self.messsageLabel.preferredMaxLayoutWidth = [UIScreen mainScreen].bounds.size.width-20;
最后我們來獲取Cell的高度:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self.tempCell heightForModel:self.dataArr[indexPath.row]];
}
運(yùn)行后遣蚀,跟方案二的效果一樣,甚至性能還不如方案2纱耻,這種方案的好處就是不需要計(jì)算高度芭梯,高度由系統(tǒng)Autolayout計(jì)算好。最后我們引入方法四弄喘,再優(yōu)化一些性能玖喘。
動態(tài)高度四:緩存高度
性能的損耗大部分都在heightForRowAtIndexPath這個(gè)方法上,我們有500條數(shù)據(jù)限次,當(dāng)我們點(diǎn)擊紅色按鈕后芒涡,會刷新tableView,這時(shí)就會調(diào)用501(加上我們新插入的數(shù)據(jù))次heightForRowAtIndexPath方法卖漫,所以每個(gè)Cell的高度都會重新算一次,這樣性能就大打折扣赠群,那我們想辦法不讓他算唄羊始,那就把計(jì)算好的高度緩存下來吧。所以我們在Model中加入一個(gè)屬性查描,用于保存Model所對應(yīng)的Cell的高度突委。所以最后我們Model中的屬性有這幾個(gè):
@interface CellModel : NSObject
// 姓名
@property (nonatomic, copy) NSString *name;
// 發(fā)言內(nèi)容
@property (nonatomic, copy) NSString *message;
// 該Model對應(yīng)的Cell高度
@property (nonatomic, assign) CGFloat cellHeight;
@end
然后我們來到TableView的Cell高度的代理方法中,如果當(dāng)前Model的cellHeight為0冬三,說明這個(gè)Cell沒有緩存過高度匀油,則計(jì)算Cell的高度,并把這個(gè)高度記錄在Model中勾笆,這樣下次再獲取這個(gè)Cell的高度敌蚜,就可以直接去Model中獲取,而不用重新計(jì)算了:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CellModel *model = self.dataArr[indexPath.row];
if (model.cellHeight == 0) {
CGFloat cellHeight = [self.tempCell heightForModel:self.dataArr[indexPath.row]];
// 緩存給model
model.cellHeight = cellHeight;
return cellHeight;
} else {
return model.cellHeight;
}
}
這樣就實(shí)現(xiàn)了高度緩存和Model窝爪、Cell都對應(yīng)的優(yōu)化弛车,我們無需手動管理高度緩存齐媒,在添加和刪除數(shù)據(jù)的時(shí)候,都是對Model在數(shù)據(jù)源中進(jìn)行添加或刪除纷跛。
最后再運(yùn)行喻括,你會發(fā)現(xiàn),紅色按鈕贫奠,怎么點(diǎn)唬血,都是60幀滿,偶爾會掉到59唤崭,那也只是極為短暫的一個(gè)時(shí)間刁品,可以忽略不計(jì),這樣浩姥,聊天室的刷新性能挑随,就可以完美的解決了。
另
以上所有測試都在iPhone6s上進(jìn)行勒叠,如果其他盆友也對TableView的性能優(yōu)化感興趣兜挨,希望可以告知我其他型號手機(jī)的運(yùn)行效果,或者如果有更高效的處理方法眯分,都可以聯(lián)系我拌汇,大家互相學(xué)習(xí)、共同進(jìn)步弊决。
最后補(bǔ)上Demo:https://github.com/ZhaoheMHz/UITableVIewSelfSizing