你們基本上都作過這樣的需求:在UITableView上展現文本,且文本內容長短不一,每一行單元格都要動態計算高度,使得單元格能夠恰好容納下須要展現的文字。爲了方便講解,咱們把文本框設定成一個距離cell上下左右均有20px間距的UILabel,須要單元格動態調整高度,使得文本框恰好能夠展現出全部的文本內容。git
需求自己並非很是複雜,實現這個需求基本上能夠採用兩種方法:github
一、代碼動態計算高度數組
二、利用iOS8中UITableView的estimatedRowHeight新特性經過約束計算高度緩存
咱們先來看一下兩種方案的實現方式:微信
在UITableViewCell的自定義類中增長一個計算cell高度的類方法,具體代碼以下:app
+ (CGFloat)calculateTitleWidth:(NSString *)title{
CGFloat stringWidth = 0;
CGSize size = CGSizeMake(kRBScreenWidth - 20.0f*2, MAXFLOAT);
if (title.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
stringWidth = [title
boundingRectWithSize:size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName:kRBTextFont}
context:nil].size.height;
#else
//iOS7.0如下方法
stringWidth = [title sizeWithFont:kRBTextFont
constrainedToSize:size
lineBreakMode:NSLineBreakByCharWrapping].height;
#endif
}
return stringWidth;
}
複製代碼
當咱們經過- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
方法獲得對應的cell以後,調用cell的- (void)buildData:(NSString *)title
方法,填充文本,設置文本框高度:佈局
- (void)buildData:(NSString *)title{
self.titleLabel.text = title;
self.titleLabel.frame = CGRectMake(20.0f, 20.0f, kRBScreenWidth - 20.0f*2, [RBAutoSizeTableViewCell calculateTitleWidth:title]);
}
複製代碼
重寫UITableViewDataSource的protocol方法,動態計算每一行的高度:性能
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return [RBAutoSizeTableViewCell calculateTitleWidth:self.titles[indexPath.row]] + 20.0*2;
}
複製代碼
先將titleLabel利用約束固定在cell上:優化
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.mas_equalTo(self.contentView).with.mas_offset(20.0f);
make.bottom.right.mas_equalTo(self.contentView).with.mas_offset(-20.0f);
}];
複製代碼
再將UITableView設置爲預估高度的模式:ui
self.estimatedRowHeight = 300.0f; //設置近似值
self.rowHeight = UITableViewAutomaticDimension;
複製代碼
只須要兩行代碼,咱們就完成了動態高度的估算工做,很是的簡潔明瞭。
這裏我用了Xib加載cell和代碼構建cell兩種方式生成cell:
//代碼建立cell
if(!autoSizeTableViewCell){
autoSizeTableViewCell = [[RBAutoSizeTableViewCell1 alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
}
//nib建立cell
if(!autoSizeTableViewCell){
autoSizeTableViewCell = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([RBAutoSizeTableViewCell2 class]) owner:self options:nil].lastObject;
}
複製代碼
儘管不少同窗都用過Xib文件,可是對於其中的原理不甚熟悉,Xib其實就是一個XML文件,在項目運行時會被編譯成二進制文件即nib文件,Fabric將會在下文中分析Xib的執行效率。
注意:千萬不要再次重寫- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
方法,不然UITableView將不會預估高度
當我接到一個需求的時候,其實腦子裏面閃現過許多實現需求的方法,到底用哪種方法,取決於不少因素:代碼複雜度,可擴展性,穩定性,代碼執行效率等等。
今天Fabric主要從性能方面來分析兩種實現方式的優劣,下面是一張三種方式動態計算高度(咱們把Xib+約束動態計算單元格高度看成第三種自適應方法),加載UITableView所需時間的柱狀圖:
正如你們看到的,代碼動態計算高度的耗時要遠遠地高於後二者,效率很是低下,當咱們把cell總數設置爲1000,甚至10000的時候,能夠很明顯的感覺到加載緩慢,嚴重的傷害了用戶體驗。
你們可能會驚訝,短短几行代碼,爲何耗時的差距能夠高達上萬倍呢?!
緣由在於:當使用代碼動態計算高度時,UITableView會首先執行一遍
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return [RBAutoSizeTableViewCell calculateTitleWidth:self.titles[indexPath.row]] + 20.0*2;
}
複製代碼
方法,當有1000個cell的時候,UITableview就會首先執行1000次計算高度的方法,而後再去執行- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
獲取cell,獲取cell以後,又會執行一次- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
方法,來獲取當前cell的高度。這樣一來,確定要耗費很是長的時間。
反觀第二種方法,UITableView只會預加載一個UITableView contentSize的內容,也就是說,不管有多少cell,UITableView會先加載一屏內容,再預計算第二屏的高度,不會有更多的計算操做。這種預加載邏輯,保障了UITableView既不會卡頓,也不會消耗更多的資源。
另外看一下Xib+約束的執行效率,並不比純代碼要低,可能有讀者會有疑問:
一、UITableView上一次性建立的Xib文件很少因此看不出性能差異。
二、Xib文件上只有一個UILabel,太簡單了,因此看不出Xib文件的耗時。
因此Fabric把行高設置成5px,讓UITableView一次性多生成一些cell;儘可能多拖拽一些控件到Xib上,增長Xib文件的複雜度,執行結果顯示: 純代碼構建cell和用Xib獲取cell沒有明顯的性能區別。所以,Xib文件的執行效率是很高的,並不像我起先設想的那樣,讀取XML文件會很耗時。
經過動態加載單元格的性能實驗,咱們知道了UITableView加載緩慢的緣由:重複執行了大量的耗時操做,所以Fabric總結了如下幾點提升UITableView加載效率的方法:
- (UIImage *)getCellImage:(NSString *)imageName{
if(!imageName) return nil;
UIImage *img = [self.imageDict objectForKey:imageName];
if(!img){
img = [UIImage imageNamed:imageName];
[self.imageDict setValue:img forKey:imageName];
}
return img;
}
複製代碼
固然,不管是第三方SDWebImage仍是系統方法+ (nullable UIImage *)imageNamed:(NSString *)name
,都已經幫咱們將圖片存儲在磁盤上了,不須要咱們再次去作緩存了,Fabric只是用圖片緩存舉個例子而已。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
方法中,獲取到cell以後再去addSubView
,若是這樣作的話,cell每一次出如今用戶界面上就add一次subView,那麼用戶來回滑動幾回UITableView,就會發現界面卡頓,滑動明顯變慢,甚至滑不動了。hidden
屬性去隱藏對用戶不可見的控件,而不是經過設置alpha爲0,或者設置控件寬高爲0的方式來隱藏控件,由於當控件的hidden
屬性爲YES的時候,系統會自動優化控件內存,減小設備的資源消耗。首先,感謝兩位讀者Asuray和ControlM給Fabric的寶貴留言。他們一針見血的指出了代碼計算高度自適應UITableView效率低下的緣由:在UITableViewDataSource的代理方法中,執行了過多的冗餘的計算UILabel高度的操做。
Fabric的方法一是一個不恰當的加載UITableView的思路,旨在讓讀者看到UITableView加載效率低下的緣由。下面咱們來設想一下如何優化,結合上文總結的五點提升UITableView加載效率的方法,Fabric想出瞭如下三點改進方法:
- (void)convertDataToModel{
[self.titles enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
RBTitleModel *titleModel = [[RBTitleModel alloc] init];
titleModel.title = obj;
titleModel.titleLabelHeight = 0.0f;
[self.titles replaceObjectAtIndex:idx withObject:titleModel];
}];
}
複製代碼
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
方法中再也不執行計算UILabel高度的方法,而是給出一個預設的高度,代碼以下:- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
RBTitleModel *model = [self.titles objectAtIndex:indexPath.row];
return model.titleLabelHeight + 20.0*2;
}
複製代碼
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
方法中計算UILabel的高度,代碼以下:- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *autoSizeTableViewCellID = @"RBAutoSizeTableViewCell";
RBAutoSizeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:autoSizeTableViewCellID];
if(!cell){
cell = [[RBAutoSizeTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:autoSizeTableViewCellID];
}
//動態計算當前cell的高度
RBTitleModel *titleModel = [self.titles objectAtIndex:indexPath.row];
[titleModel calculateTitleWidth];
[cell buildData:self.titles[indexPath.row]];
return cell;
}
複製代碼
在計算高度時,Fabric採用了緩存機制,若是titleModel.titleHeight的數值不爲0,說明已經計算太高度不須要重複計算,代碼以下:
- (void)calculateTitleWidth{
//有緩存則不須要重複計算
if(self.titleLabelHeight > 0) return;
CGFloat stringWidth = 0;
CGSize size = CGSizeMake(kRBScreenWidth - 20.0f*2, MAXFLOAT);
if (self.title.length > 0) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
stringWidth = [self.title
boundingRectWithSize:size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName:kRBTextFont}
context:nil].size.height;
#else
//iOS7.0如下方法
stringWidth = [self.title sizeWithFont:kRBTextFont
constrainedToSize:size
lineBreakMode:NSLineBreakByCharWrapping].height;
#endif
}
self.titleLabelHeight = stringWidth;
}
複製代碼
通過改進以後,UITableView的執行效率明顯變高了,下圖是改進以後的兩種方式的UITableView加載耗時的柱狀圖:
Fabric能想到的優化UITableView加載效率的方法就只有以上這麼多了,歡迎你們在文章下方留言一塊兒探討,也能夠加個人微信justlikeitRobert和我討論,喜歡這篇文章請點贊,謝謝你們的關注與支持。