前言
對于TableViewCell的高度自適應(yīng)疲酌,很多初次接觸的同學(xué),還是很頭痛的擂仍。就算已經(jīng)有些開發(fā)經(jīng)驗的同學(xué),處理起來也可能用錯了方法。但其實系統(tǒng)已經(jīng)提供了很方便的處理方法悄雅,我們這里就系統(tǒng)的高度計算做一個講解。然后主要要講的铁蹈,是我在實際開發(fā)中(我們App加入了直播功能宽闲,直播中要處理大量的聊天消息)用到的方法,也是在性能上優(yōu)化了很多的方法握牧,將計算好的高度緩存下來容诬,在大量數(shù)據(jù)(幾百、幾千條數(shù)據(jù))進(jìn)行刷新沿腰、插入數(shù)據(jù)览徒、刪除數(shù)據(jù)等操作的時候也能保證性能、流暢性颂龙,而相比于其他高度緩存方案习蓬,這種方式的高度緩存纽什,更方便管理。以下高度都結(jié)合Masonry來完成(畢竟手寫Autolayout還是Masonry比較方便)躲叼,使用XIB的同學(xué)稿湿,也可以直接拖約束。
場景模擬
我們寫個Demo押赊,來模擬下直播聊天室中情況饺藤,眾所周知,直播聊天室中的消息量是巨大的流礁,而且刷新特別快涕俗,在刷新聊天列表的時候,最耗費(fèi)性能的就是UITableView的兩個代理方法神帅,一個heightForRowAtIndexPath再姑,一個cellForRowAtIndexPath。無論是刷新還是新增找御、刪除元镀,都會反復(fù)觸發(fā)這兩個方法,而對于聊天室霎桅,如果從后面追加數(shù)據(jù)栖疑,假設(shè)你原來有1000條數(shù)據(jù),即使你從后面insert一個cell滔驶,那也會調(diào)用1000次HeightForRow遇革,如果你在計算高度的時候,使用了很復(fù)雜的計算方式揭糕,就很影響性能了萝快。
首先新建個項目,然后在項目中加入Masonry著角,再然后加入一個顯示當(dāng)前屏幕FPS的label進(jìn)來揪漩,提取自YYKit,YYFPSLabel吏口。這樣就能大致了解性能如何了奄容。然后我們在ViewController.m中加入這個控件:
- (void)viewDidLoad {? ? [superviewDidLoad];? ? ? ? YYFPSLabel *fpsLabel = [[YYFPSLabel alloc] initWithFrame:CGRectMake(0,20,60,20)];? ? [self.view addSubview:fpsLabel];}
運(yùn)行后我們的Demo頂部就會顯示FPS了:
Paste_Image.png
然后我們先建一個Model,和一個Cell锨侯,Model代表我們從服務(wù)器請求的數(shù)據(jù)模型嫩海,Cell就是我們要用到的展示內(nèi)容的Cell。為了讓Cell更符合實際項目的需求囚痴,我們讓cell顯示多一些的內(nèi)容叁怪,來一個拼接的屬性字符串吧。
新建個Model:
Paste_Image.png
模擬聊天中的消息展示深滚,我們給Model兩個屬性奕谭,一個姓名涣觉,一個發(fā)言內(nèi)容:
// 姓名@property(nonatomic,copy)NSString*name;// 發(fā)言內(nèi)容@property(nonatomic,copy)NSString*message;
我們再新建一個Cell,在Cell中將內(nèi)容展示出來:
Paste_Image.png
我們的Cell中只有一個Label血柳,用于展示“姓名:發(fā)言內(nèi)容”這樣的內(nèi)容官册,注意這里布局,采用自動布局难捌,Cell的ContentView由Label中的內(nèi)容撐開:
@interfaceMessageCell()@property(nonatomic,strong)UILabel*messsageLabel;@end@implementationMessageCell- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString*)reuseIdentifier {if(self== [superinitWithStyle:style reuseIdentifier:reuseIdentifier]) {// 創(chuàng)建UI[selfcreateUI];? ? }returnself;}- (void)createUI {/** 發(fā)言 */self.messsageLabel = [[UILabelalloc] 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)建一個可變屬性字符串NSMutableAttributedString*finalStr = [[NSMutableAttributedStringalloc] init];// 創(chuàng)建姓名NSAttributedString*nameStr = [[NSAttributedStringalloc] initWithString:message.name attributes:@{NSFontAttributeName: [UIFontsystemFontOfSize:16],NSForegroundColorAttributeName: [UIColorredColor]}];// 創(chuàng)建發(fā)言內(nèi)容NSAttributedString*messageStr = [[NSAttributedStringalloc] initWithString:message.message attributes:@{NSFontAttributeName: [UIFontsystemFontOfSize:16],NSForegroundColorAttributeName: [UIColorblackColor]}];// 拼接上兩個字符串[finalStr appendAttributedString:nameStr];? ? [finalStr appendAttributedString:messageStr];self.messsageLabel.attributedText = finalStr;}@end
這里我們需要注意的是膝宁,Label要高度自適應(yīng)的撐開Cell的ContentView的高度。然后我們?nèi)iewController中添加一個用于展示這些內(nèi)容的TableView根吁,在viewDidLoad方法的結(jié)尾员淫,我們添加一個按鈕,該按鈕模擬聊天室中接收到了新消息击敌,并滾動到TableView的最底部介返。具體代碼如下:
@interfaceViewController() @property(nonatomic,strong)UITableView*tableView;@property(nonatomic,strong)NSMutableArray*dataArr;@end@implementationViewController- (void)viewDidLoad {? ? [superviewDidLoad];? ? ? ? YYFPSLabel *fpsLabel = [[YYFPSLabel alloc] initWithFrame:CGRectMake(0,20,60,20)];? ? [self.view addSubview:fpsLabel];// 創(chuàng)建TableViewself.tableView = [[UITableViewalloc] 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:[MessageCellclass] forCellReuseIdentifier:@"MessageCell"];// 模擬一些數(shù)據(jù)源NSArray*nameArr = @[@"張三:",@"李四:",@"王五:",@"陳六:",@"吳老二:"];NSArray*messageArr = @[@"ash快點回家愛是妒忌哈市黨和國家按時到崗哈時代光華撒國會大廈國會大廈國會大廈更好的噶山東黃金撒旦哈安師大噶是個混蛋撒",@"傲世江湖點撒恭候大駕水草瑪瑙現(xiàn)在才明白你個壞蛋擦邊沙塵暴你先走吧出現(xiàn)在",@"撒點花噶閃光燈",@"按時間大公司大概好久撒大概好久撒黨和國家按時到崗哈師大就薩達(dá)數(shù)據(jù)庫化打算幾點撒謊就看電視驕傲的撒金葵花打暑假工大撒比的撒謊講大話手機(jī)巴士差距啊市場報價啊山東黃金as擦傷擦啊as擦肩時擦市場報價按時VC阿擦把持啊三重才撒啊雙層巴士吃按時吃啊雙層巴士擦報啥錯",@"as大帥哥大孤山街道安師大好噶?xí)r間過得撒黃金國度"];// 向數(shù)據(jù)源中隨機(jī)放入500個Modelself.dataArr = [[NSMutableArrayalloc] init];for(inti=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)建一個按鈕,點擊可從后面追加一些數(shù)據(jù)進(jìn)來UIButton*button = [[UIButtonalloc] initWithFrame:CGRectMake(0,40,100,60)];? ? button.backgroundColor = [UIColorredColor];? ? [self.view addSubview:button];? ? [button addTarget:selfaction:@selector(addData) forControlEvents:UIControlEventTouchUpInside];}- (void)addData {// 添加一個Model沃斤,在追加到Tableview中CellModel *model = [[CellModel alloc] init];? ? model.name =@"皮皮:";? ? model.message =@"安師大公司的嘎斯大時代安師大嘎斯高大上撒旦嘎嘎就是打閃光燈";? ? [self.dataArr addObject:model];// 插入到tableView中[self.tableView insertRowsAtIndexPaths:@[[NSIndexPathindexPathForRow:self.dataArr.count-1inSection:0]] withRowAnimation:UITableViewRowAnimationNone];// 再滾動到最底部[self.tableView scrollToRowAtIndexPath:[NSIndexPathindexPathForRow:self.dataArr.count-1inSection:0] atScrollPosition:UITableViewScrollPositionBottomanimated:YES];}- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section {returnself.dataArr.count;}- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {return44;}- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {? ? MessageCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MessageCell"forIndexPath:indexPath];? ? [cell setMessage:self.dataArr[indexPath.row]];returncell;}@end
效果如下圣蝎,這里我們固定Cell高度為44了,所以全程怎么滾動衡瓶,F(xiàn)PS都是60:
9BAB5AB9072DACA29A7084C28B42DDA9.png
動態(tài)高度一:系統(tǒng)自帶支持
那好了徘公,上面的固定高度測試完了,我們來測試下適配Cell高度的方法鞍陨。首先采用系統(tǒng)的動態(tài)高度方法步淹。
我們需要做兩件事:第一:指定TableView的高度為自適應(yīng):
// 必須設(shè)置預(yù)估高度才能生效self.tableView.estimatedRowHeight =100;self.tableView.rowHeight =UITableViewAutomaticDimension;
第二:將TableView的行高代理方法注釋掉,也就是下面這個方法:
//- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {//? ? return 44;//}
這時再運(yùn)行诚撵,你會發(fā)現(xiàn),Cell的高度已經(jīng)自動適配键闺,滾動中也特別流暢寿烟,保持60幀:
Paste_Image.png
但如果點擊我們的紅色按鈕,就卡爆了辛燥,而且會有一個刷新的白屏:
Paste_Image.png
實測系統(tǒng)的這個方法筛武,只適用于iOS8及以上,且在數(shù)據(jù)量超大的時候挎塌,進(jìn)行插入和刪除徘六,都是很不流暢的,不建議采用榴都。當(dāng)然這種方法針對一些常用場景待锈,比如新聞列表、商品列表什么的嘴高,數(shù)據(jù)量沒那么大且不涉及到新增竿音、刪除數(shù)據(jù)的時候和屎,這種方法,還是蠻不錯的春瞬,寫起來很簡便柴信。
動態(tài)高度二:自己計算高度
我們將上面的方法撤回,試驗下自己計算Cell高度宽气,性能如何随常。
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {// 創(chuàng)建一個可變屬性字符串NSMutableAttributedString*finalStr = [[NSMutableAttributedStringalloc] init];// 取出ModelCellModel *message =self.dataArr[indexPath.row];// 創(chuàng)建姓名NSAttributedString*nameStr = [[NSAttributedStringalloc] initWithString:message.name attributes:@{NSFontAttributeName: [UIFontsystemFontOfSize:16],NSForegroundColorAttributeName: [UIColorredColor]}];// 創(chuàng)建發(fā)言內(nèi)容NSAttributedString*messageStr = [[NSAttributedStringalloc] initWithString:message.message attributes:@{NSFontAttributeName: [UIFontsystemFontOfSize:16],NSForegroundColorAttributeName: [UIColorblackColor]}];// 拼接上兩個字符串[finalStr appendAttributedString:nameStr];? ? [finalStr appendAttributedString:messageStr];// 計算高度CGSizesize = [finalStr boundingRectWithSize:CGSizeMake(self.view.frame.size.width-20,CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigincontext:nil].size;returnceil(size.height);}
這種方式,在滾動列表的時候萄涯,還是60幀流暢的线罕,點擊紅色按鈕后,會降到47幀窃判,并持續(xù)一小段時間钞楼,所以這段時間中,你如果是在聊天室中播放彈幕袄琳,或者進(jìn)行點贊動畫的處理的時候询件,這些內(nèi)容都會卡住,直到這段時間過去唆樊,當(dāng)然相比于系統(tǒng)的方法宛琅,性能還是稍好一點的:
Paste_Image.png
動態(tài)高度三:Autolayout計算高度
有人可能覺得,上面計算高度太麻煩了逗旁,不就是把Cell中setMessage拿出來再寫一遍嘛嘿辟,同樣的代碼不要寫兩次,那我們換種方式來寫片效。這里我們先給ViewController這個Controller加一個屬性红伦,下面的這個Cell,承擔(dān)了計算Cell高度的工作:
@property(nonatomic,strong) MessageCell *tempCell;
在viewDidLoad中初始化:
self.tempCell = [[MessageCell alloc] initWithStyle:0reuseIdentifier:@"MessageCell"];
然后我們給Cell加個方法淀衣,這里需要注意的是昙读,我們要對最終算出來的高度加1,這個1是Cell的分割線的高度膨桥,當(dāng)前如果你隱藏了分割線蛮浑,就不需要加這個1了:
// 根絕數(shù)據(jù)計算cell的高度- (CGFloat)heightForModel:(CellModel *)message {? ? [selfsetMessage:message];? ? [selflayoutIfNeeded];CGFloatcellHeight = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height+1;returncellHeight;}
還要指定Cell中的Label的最大寬度,保證在適配Label的時候只嚣,不會超出這個寬度:
self.messsageLabel.preferredMaxLayoutWidth = [UIScreenmainScreen].bounds.size.width-20;
最后我們來獲取Cell的高度:
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {return[self.tempCell heightForModel:self.dataArr[indexPath.row]];}
運(yùn)行后沮稚,跟方案二的效果一樣,甚至性能還不如方案2册舞,這種方案的好處就是不需要計算高度蕴掏,高度由系統(tǒng)Autolayout計算好。最后我們引入方法四,再優(yōu)化一些性能囚似。
動態(tài)高度四:緩存高度
性能的損耗大部分都在heightForRowAtIndexPath這個方法上剩拢,我們有500條數(shù)據(jù),當(dāng)我們點擊紅色按鈕后饶唤,會刷新tableView徐伐,這時就會調(diào)用501(加上我們新插入的數(shù)據(jù))次heightForRowAtIndexPath方法,所以每個Cell的高度都會重新算一次募狂,這樣性能就大打折扣办素,那我們想辦法不讓他算唄,那就把計算好的高度緩存下來吧祸穷。所以我們在Model中加入一個屬性性穿,用于保存Model所對應(yīng)的Cell的高度。所以最后我們Model中的屬性有這幾個:
@interfaceCellModel:NSObject// 姓名@property(nonatomic,copy)NSString*name;// 發(fā)言內(nèi)容@property(nonatomic,copy)NSString*message;// 該Model對應(yīng)的Cell高度@property(nonatomic,assign)CGFloatcellHeight;@end
然后我們來到TableView的Cell高度的代理方法中雷滚,如果當(dāng)前Model的cellHeight為0需曾,說明這個Cell沒有緩存過高度,則計算Cell的高度祈远,并把這個高度記錄在Model中呆万,這樣下次再獲取這個Cell的高度,就可以直接去Model中獲取车份,而不用重新計算了:
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {? ? CellModel *model =self.dataArr[indexPath.row];if(model.cellHeight ==0) {CGFloatcellHeight = [self.tempCell heightForModel:self.dataArr[indexPath.row]];// 緩存給modelmodel.cellHeight = cellHeight;returncellHeight;? ? }else{returnmodel.cellHeight;? ? }}
這樣就實現(xiàn)了高度緩存和Model谋减、Cell都對應(yīng)的優(yōu)化,我們無需手動管理高度緩存扫沼,在添加和刪除數(shù)據(jù)的時候出爹,都是對Model在數(shù)據(jù)源中進(jìn)行添加或刪除。
最后再運(yùn)行缎除,你會發(fā)現(xiàn)严就,紅色按鈕,怎么點伴找,都是60幀滿盈蛮,偶爾會掉到59,那也只是極為短暫的一個時間技矮,可以忽略不計,這樣殊轴,聊天室的刷新性能衰倦,就可以完美的解決了。
另
以上所有測試都在iPhone6s上進(jìn)行旁理,如果其他盆友也對TableView的性能優(yōu)化感興趣樊零,希望可以告知我其他型號手機(jī)的運(yùn)行效果,或者如果有更高效的處理方法,都可以聯(lián)系我驻襟,大家互相學(xué)習(xí)夺艰、共同進(jìn)步。
最后補(bǔ)上Demo:https://github.com/ZhaoheMHz/UITableVIewSelfSizing