配合Masonry實現(xiàn)TableViewCell的高度自適應(yīng)汁蝶,以及更易管理的高度緩存

前言

對于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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沉衣,一起剝皮案震驚了整個濱河市郁副,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌豌习,老刑警劉巖存谎,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異肥隆,居然都是意外死亡既荚,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門栋艳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恰聘,“玉大人,你說我怎么就攤上這事吸占∏邕叮” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵旬昭,是天一觀的道長篙螟。 經(jīng)常有香客問我,道長问拘,這世上最難降的妖魔是什么遍略? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮骤坐,結(jié)果婚禮上绪杏,老公的妹妹穿的比我還像新娘。我一直安慰自己纽绍,他們只是感情好蕾久,可當(dāng)我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拌夏,像睡著了一般僧著。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上障簿,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天盹愚,我揣著相機(jī)與錄音,去河邊找鬼站故。 笑死皆怕,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播愈腾,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼憋活,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了虱黄?” 一聲冷哼從身側(cè)響起悦即,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎礁鲁,沒想到半個月后盐欺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡仅醇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年冗美,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片析二。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡粉洼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出叶摄,到底是詐尸還是另有隱情属韧,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布蛤吓,位于F島的核電站宵喂,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏会傲。R本人自食惡果不足惜锅棕,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望淌山。 院中可真熱鬧裸燎,春花似錦、人聲如沸泼疑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽退渗。三九已至移稳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間会油,已是汗流浹背秒裕。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留钞啸,地道東北人。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像体斩,于是被迫代替她去往敵國和親梭稚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容