實戰UITableview深度優化

演示項目下載地址:https://github.com/YYProgrammer/YYTableViewDemo git

項目裏的低性能版是常規寫法實現的tableview,高性能版是作了相關優化後的tableview。github

tableView滑動爲何會卡?api

咱們能夠想象這樣一個場景:數組

有一個老師、學生A、學生B、一個畫板、一個櫥窗。緩存

每一秒鐘,老師都要告訴學生A一個題目讓他們做畫,學生A負責研究這個題目表達的含義,而後告訴學生B應該畫什麼,學生B收到消息後,在畫板上畫出對應的畫,在這一秒鐘結束之時,把畫貼到櫥窗,供外面的人觀看。而後繼續下一秒的審題、畫畫的步驟。網絡

正常狀況下,學生A、B都能合同愉快,在規定的時間畫好,但有時候,學生A審題過久,或者這一秒的量太多,學生B畫得不夠快,那麼這一秒,甚至下幾秒,櫥窗裏的畫會保持上一次的畫,直到他們畫好下一張。框架

這裏,異步

學生A就是CPU,負責視圖相關的計算工做並告知GPU應該怎麼繪圖;async

學生B就是GPU,進行圖形的繪製、渲染等工做;工具

「每一秒鐘」就是屏幕刷新週期,一般是1/60秒,即每秒屏幕刷新60次;

櫥窗就是手機屏幕,用來顯示GPU繪製好的內容;

「畫得不夠快,致使櫥窗的畫在接下來的幾秒裏一直是上一次的畫」的狀況,就是掉幀,就是卡的緣由。

能夠看出,不管是CPU,仍是GPU的壓力過大,都會在一個週期內完不成工做,都會致使掉幀的狀況發生。

而在tableview滑動時,會頻繁出現對象建立、屬性修改、佈局計算、文本繪製、圖形生成等消耗資源的操做發生。

因此優化,就是想辦法在這一秒的時間裏,減輕它們的負荷,保證每一次都能「把畫兒畫完」。

優化的思路

首先咱們來看看下面這個tableview的流程:

  1. 獲取數據;

  2. 把數據轉化成model、存進數組;

  3. tableview調用reloadData刷新數據;

  4. 在代理方法cellForRowAtIndexPath裏,建立自定義的cell,把model賦值給cell;

  5. cell在對應的model的set方法裏,根據拿到的model,設置圖片的image,設置label的text等(控件都以懶加載形式初始化);

  6. 在代理方法heightForRowAtIndexPath裏,根據model,算出當前行應該顯示多少的高度;

  7. 在cell的layoutSubviews方法裏,佈局子控件。

一、避免主線程阻塞

1/2步裏的獲取數據、數據處理等耗時操做,應該放入後臺線程異步處理,處理好後再通知主線程刷新界面。

經常使用的網絡請求框架都是在後臺線程完成的數據請求,但有時咱們會忘了,在這些請求的回調裏操做數據時,是在主線程裏進行的操做,須要咱們手動管理線程。

例如:AFNetworking使用時

[[AFHTTPSessionManager manager] POST:@"" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        //移到異步線程作
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            //一、字典轉模型
            //二、計算每一個model的數據,佈局參數等。
            dispatch_async(dispatch_get_main_queue(), ^{
                //三、回到主線程,刷新tableview等
            });
        });
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
         
    }];

總之是能在異步操做的,都異步操做。

一般來講,UIKitCoreAnimation相關操做必須在主線程中進行,其它的能夠在後臺線程異步執行。比方說圖像的異步繪製等,具體的後面介紹。

二、避免頻繁的對象建立

對象的建立會發送內存分配、屬性調整等。

因此,首先,儘可能用輕量的對象代替重量的對象。好比CALayer代替UIView。

接着,多利用緩存思想,對象建立後緩存起來,須要的時候再拿出來用。合理利用內存開銷,減小CPU開銷。

關於這一點,系統已經提供了很好的api來作cell的緩存

[tableView dequeueReusableCellWithIdentifier:ID];

但咱們有時會忘了這樣一種狀況:

如圖,這個label顯示的內容由model的兩個參數(時間、千米數)拼接而成,咱們習慣在cell裏model的set方法中這樣賦值

//時間
    NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
    formatter.dateStyle = NSDateFormatterMediumStyle;
    formatter.timeStyle = NSDateFormatterShortStyle;
    [formatter setDateFormat:@"yyyy年MM月"];
    NSDate* date = [NSDate dateWithTimeIntervalSince1970:[model.licenseTime intValue]];
    NSString* licenseTimeString = [formatter stringFromDate:date];
    //千米數
    NSString *travelMileageString = (model.travelMileage != nil && ![model.travelMileage isEqualToString:@""]) ? [NSString stringWithFormat:@"%@萬千米",model.travelMileage] : @"里程暫無";
    //賦值給label.text
    self.carDescribeLabel.text = [NSString stringWithFormat:@"%@ / %@",licenseTimeString,travelMileageString];

在tableview滾動的過程當中,這些對象就會被來回的建立,而且這個計算過程是在主線程裏被執行的。

咱們能夠把這些操做,移到第2步(字典轉模型)來作,計算好這個label須要顯示的內容,做爲屬性存進model中,須要的時候直接用。

這樣,既能夠避免主線程的阻塞,又能夠避免對象的頻繁建立。

而下面這個例子也是緩存思想的體現:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 15.0 + 80.0 + 15.0;
}
修改成
static float ROW_HEIGHT = 15.0 + 80.0 + 15.0;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return ROW_HEIGHT;
}

固然這不是減小對象的建立,而是減小了計算的次數,減小了頻繁調用方法裏的邏輯,從而達到更快的速度。

三、減小對象的屬性賦值操做

尤爲是UIView的frame/bounds等屬性的賦值操做,會產生比較大的CPU消耗。

對象的調整也常常是消耗 CPU 資源的地方。這裏特別說一下 CALayer:CALayer 內部並無屬性,當調用屬性方法時,它內部是經過運行時 resolveInstanceMethod 爲對象臨時添加一個方法,並把對應屬性值保存到內部的一個 Dictionary 裏,同時還會通知 delegate、建立動畫等等,很是消耗資源。UIView 的關於顯示相關的屬性(好比 frame/bounds/transform)等實際上都是 CALayer 屬性映射來的,因此對 UIView 的這些屬性進行調整時,消耗的資源要遠大於通常的屬性。對此你在應用中,應該儘可能減小沒必要要的屬性修改。

——摘自iOS 保持界面流暢的技巧

因此在cell的layoutSubviews里布局全部子控件對性能是有影響的,對於frame固定的UIView,在cell建立時(或者懶加載方法裏)佈局一次便可。

另外,有時候一個tableview的cell的樣式存在頻繁的變化但又有必定的規律(比方說有一個label的高度老是在兩行、一行來回變化),這就免不了會頻繁的設置它的高度。若是追求很高的性能,能夠篩分紅兩個cell,從而避免頻繁的更改frame。

四、異步繪製

文本渲染、圖像繪製都是比較消耗性能的操做,而UILabel等控件都是在主線程進行的文本繪製。這會對性能產生比較大的影響。

UIKit和CoreAnimation相關操做必須在主線程中進行,其它的能夠在後臺線程異步執行

怎麼來簡單理解這句話呢?

比方說:爲一個UIImageView設置image,

imageView.image = image;

以上代碼必須在主線程進行,但這個image的繪製過程,能夠在異步線程作

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    CGContextRef ctx = CGBitmapContextCreate(...);
    // 吧啦吧啦繪圖
    CGImageRef imgRef = CGBitmapContextCreateImage(ctx);//位圖
    UIImage *image = [UIImage imageWithCGImage:imgRef];//轉成UIImage
    dispatch_async(dispatch_get_main_queue(), ^{
        //回到主線程
        imageView.image = image;//設置imageView的image
    });
});

因此異步繪製的思想,就是儘可能把須要顯示的內容,在異步線程繪製,繪製好後再通知主線程顯示

在這個項目裏VVeboTableViewDemo,做者把cell裏不少須要顯示的內容都異步繪製成圖片再顯示,並實現了一個異步繪製的Label,是異步繪製思想一個很好的例子。

的確,優化性能會犧牲一些開發速度,那麼如何相對高效的利用異步繪製技術呢

推薦使用YYKit的相關組件,例如YYLabel。

YYLabel是一個能夠異步繪製的用來顯示文字的控件,它能夠像UILabel如出一轍的使用,也能夠經過賦值它的textLayout(一個YYTextLayout對象)來顯示內容,第二種方式擁有更高的性能。

舉個例子,通常來講咱們是這樣來顯示一段文字的

/** cell的.m文件 */
//懶加載一個UILabel
- (UILabel *)carVersionLabel
{
    if (!_carVersionLabel)
    {
        _carVersionLabel = [[UILabel alloc] init];
        [self.contentView addSubview:_carVersionLabel];
        _carVersionLabel.backgroundColor = self.contentView.backgroundColor;
        _carVersionLabel.font = [UIFont fontWithName:MAIN_CELL_TITLE_FONT_NAME size:15];
        _carVersionLabel.textColor = BLACK_TEXT_COLOR;
        _carVersionLabel.numberOfLines = 0;
        _carVersionLabel.textAlignment = NSTextAlignmentLeft;
    }
    return _carVersionLabel;
}
//model的set方法
- (void)setModel:(YYLowPerCarModel *)model
{
    _model = model;
    self.carVersionLabel.text = model.carName;
}

用YYLabel來重構的話,

/** model的.h文件 */
//聲明YYTextLayout對象
@property (nonatomic,strong) YYTextLayout *carVersionLabelLayout;//車型Label的layout

/** model的.m文件 */
//這個方法在數據請求的方法裏調用,字典轉model完成後,調用這個方法來計算一些佈局用的參數
- (void)setupViewModel
{
    //車型佈局參數
    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:self.carName];
    text.color = BLACK_TEXT_COLOR;
    text.font = CAR_VERSION_LABEL_FONT;
    text.lineSpacing = -4;
    YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(CAR_VERSION_LABEL_WIDTH, MAXFLOAT)];
    self.carVersionLabelLayout = [YYTextLayout layoutWithContainer:container text:text];
}

/** cell的.m文件 */
//懶加載Label
- (YYLabel *)carVersionLabel
{
    if (!_carVersionLabel)
    {
        _carVersionLabel = [[YYLabel alloc] init];
        [self.contentView addSubview:_carVersionLabel];
        _carVersionLabel.displaysAsynchronously = YES;//是否異步繪製
        _carVersionLabel.ignoreCommonProperties = YES;//經過設置textLayout來佈局時,設置這個參數爲YES能夠得到更高的性能
        _carVersionLabel.fadeOnHighlight = NO;//高亮漸變效果
        _carVersionLabel.fadeOnAsynchronouslyDisplay = NO;//異步繪製漸變效果
    }
    return _carVersionLabel;
}
//model的set方法
- (void)setModel:(YYLowPerCarModel *)model
{
    _model = model;
    self.carVersionLabel.textLayout = model.carVersionLabelLayout;//設置layout,異步繪製
}

若是cell裏的label都用YYLabel來實現的話,性能會獲得顯著的提高。

關於YYLabel或者YYkit相關組件的使用,還須要多實踐踩坑、看博客、看YYKit的demo,感謝巨人的肩膀。

五、簡化視圖結構

GPU在繪製圖像前,會把重疊的視圖進行混合,視圖結構越複雜,這個操做就越耗時,若是存在透明視圖,混合過程會更加複雜。

因此,咱們能夠

  • 儘可能避免複雜的圖層結構

  • 少使用透明的視圖

  • 不透明的視圖,設置opaque = YES

  • 或者採用VVeboTableViewDemo的方法,把視圖異步繪成一張圖

六、減小離屏渲染

  • 什麼是離屏渲染?

回到文章開頭的那個例子,同窗B在畫板上畫畫,這個畫板,叫作屏幕緩衝區,通常的狀況,GPU的渲染操做是在當前用於顯示的屏幕緩衝區中進行,這個叫作當前屏幕渲染(On-Screen Rendering),而因爲某些特定條件,GPU在當前屏幕緩衝區之外新開闢一個緩衝區進行渲染操做,就是離屏渲染(Off-Screen Rendering)

  • 離屏渲染爲何耗性能?

  • 建立新緩衝區

要想進行離屏渲染,首先要建立一個新的緩衝區。

  • 上下文切換

離屏渲染的整個過程,須要屢次切換上下文環境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束之後,將離屏緩衝區的渲染結果顯示到屏幕上有須要將上下文環境從離屏切換到當前屏幕。而上下文環境的切換是要付出很大代價的。

——摘自iOS 事件處理機制與圖像渲染過程

  • 離屏渲染觸發條件

--shouldRasterize(光柵化)

--masks(遮罩)

--shadows(陰影)

--edge antialiasing(抗鋸齒)

--group opacity(不透明)

--複雜形狀設置圓角等

--漸變

  • 怎麼查看哪些控件發生了離屏渲染?

利用Xcode自帶的Instruments工具來觀察。

而後觀察手機屏幕,黃色標識的地方,就發生了離屏渲染。

  • 老生常談之圓角問題

圓角是開發中常用到的美化方式,但通常的設置cornerRadius時會配合masksToBounds屬性,這就會形成離屏渲染。

關於這種問題的處理,大體有兩個思路

一、異步繪製一張圓角的圖片來顯示;

二、用一個圓角而中空的圖來蓋住。

演示項目裏我選擇了使用YYKit裏的組件來切割圖片的圓角。

其它小tips

  • 一、tableview須要刷新數據時,使用

[tableview beginUpdates];
[tableview insertRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationNone];
[tableview endUpdates];

而非

[tableview reloadData];

主要緣由在於:

一、刷新更少的行,減小cpu壓力;

二、使用YYLabel等異步繪製label時,使用reloadData會把以前的row也重繪一次,會形成「Label閃了一下的感受」。

  • 二、NSDateFormatter這個對象的相關操做很費時,須要避免頻繁的建立和計算

  • 三、對於固定行高的cell

tableview.rowHeight = 50.0;

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 50.0;
}

效率更高。

  • 四、Autolayout使用在越複雜的界面,CPU越吃力

相關文章
相關標籤/搜索