iOS UITableView性能優化

前言

  • UITableView是我們經常會使用的控件,那么關于這塊的優化還是很有必要,網上關于這塊優化的資料很多,其實核心本質還是降低 CPU和GPU 的工作來提升性能

CPU:對象的創建和銷毀、對象屬性的調整、布局計算、文本的計算和排版、圖片的格式轉換和解碼、圖像的繪制
GPU:紋理的渲染

CPU層面優化

1.用輕量級對象

比如用不到事件處理的地方,可以考慮使用 CALayer 取代 UIView

CALayer * imageLayer = [CALayer layer];
imageLayer.bounds = CGRectMake(0,0,200,100);
imageLayer.position = CGPointMake(200,200);
imageLayer.contents = (id)[UIImage imageNamed:@"xx.jpg"].CGImage;
imageLayer.contentsGravity = kCAGravityResizeAspect;
[tableCell.contentView.layer addSublayer:imageLayer];

2.不要頻繁地調用 UIView 的相關屬性

比如 frame、bounds、transform 等屬性,盡量減少不必要的修改
不要給UITableViewCell動態添加subView,可以在初始化UITableViewCell的時候就將所有需要展示的添加完畢,然后根據需要來設置hidden屬性顯示和隱藏

3.提前計算好布局

UITableViewCell高度計算主要分為兩種,一種固定高度,另外一種動態高度.

固定高度:

rowHeight高度默認44
對于固定高度直接采用self.tableView.rowHeight = 77tableView:heightForRowAtIndexPath:更高效

動態高度:

采用tableView:heightForRowAtIndexPath:這種代理方式,設置這種代理之后rowHeight則無效,需要滿足以下三個條件

  • 使用Autolayout進行UI布局約束(要求cell.contentView的四條邊都與內部元素有約束關系)
  • 指定TableView的estimatedRowHeight屬性的默認值
  • 指定TableView的rowHeight屬性為UITableViewAutomaticDimension
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44;

除了提高cell高度的計算效率之外,對于已經計算出的高度,我們需要進行緩存

4.直接設置frame

Autolayout 會比直接設置 frame 消耗更多的 CPU 資源

5.圖片尺寸合適

圖片的 size 最好剛好跟 UIImageView 的 size 保持一致
圖片通過contentMode處理顯示,對tableview滾動速度同樣會造成影響

  • 從網絡下載圖片后先根據需要顯示的圖片大小切/壓縮成合適大小的圖,每次只顯示處理過大小的圖片,當查看大圖時在顯示大圖。
  • 服務器直接返回預處理好的小圖和大圖以及對應的尺寸最好
/// 根據特定的區域對圖片進行裁剪
+ (UIImage*)kj_cutImageWithImage:(UIImage*)image Frame:(CGRect)cropRect{
    return ({
        CGImageRef tmp = CGImageCreateWithImageInRect([image CGImage], cropRect);
        UIImage *newImage = [UIImage imageWithCGImage:tmp scale:image.scale orientation:image.imageOrientation];
        CGImageRelease(tmp);
        newImage;
    });
}

6.控制最大并發數量

控制一下線程的最大并發數量,當下載線程數超過2時,會顯著影響主線程的性能。因此在使用ASIHTTPRequest時,可以用一個NSOperationQueue來維護下載請求,并將其最大線程數目maxConcurrentOperationCount
NSURLRequest可以配合 GCD進階技巧分享 來實現,或者使用NSURLConnection的setDelegateQueue:方法。
當然在不需要響應用戶請求時,也可以增加下載線程數來加快下載速度:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (!decelerate) self.queue.maxConcurrentOperationCount = 5;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    self.queue.maxConcurrentOperationCount = 5;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    self.queue.maxConcurrentOperationCount = 2;
}

7.子線程處理

盡量把耗時的操作放到子線程

  • 文本處理(尺寸計算、繪制)
  • 圖片處理(解碼、繪制)

8.預渲染圖像

顯示圖像時,解壓和重采樣會消耗很多CPU時間,
當有圖像時,在bitmap context先將其畫一遍,導出成UIImage對象,然后再繪制到屏幕,這會大大提高渲染速度,

- (void)awakeFromNib {
    if (self.image == nil) {
        self.image = [UIImage imageNamed:@"xxx"];
        UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0);
        [image drawInRect:imageRect];
        self.image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    }
}

9.異步繪制

異步繪制,就是異步在畫布上繪制內容,將復雜的繪制過程放到后臺線程中執行,然后在主線程顯示


image.png
// 異步繪制,切換至子線程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    UIGraphicsBeginImageContextWithOptions(size, NO, scale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    // TODO:draw in context...
    CGImageRef imgRef = CGBitmapContextCreateImage(context);
    UIGraphicsEndImageContext();
    dispatch_async(dispatch_get_main_queue(), ^{
        self.layer.contents = imgRef;
    });
});

這篇文章 iOS-UIView異步繪制 介紹的滿詳細
當然還是少不了YY大神的佳作,iOS 保持界面流暢的技巧(轉載)

10.按需求加載

滑動UITableView時,按需加載對應的內容

//按需加載 - 如果目標行與當前行相差超過指定行數,只在目標滾動范圍的前后指定3行加載。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
    NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
    NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
    NSInteger skipCount = 10;
    if (labs(cip.row-ip.row) > skipCount) {
        NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
        NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
        if (velocity.y < 0) {
            NSIndexPath *indexPath = [temp lastObject];
            if (indexPath.row > 3) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
            }
        }
        [self.needLoadDatas addObjectsFromArray:arr];
    }
}

還需要在tableView:cellForRowAtIndexPath:方法中加入判斷

if (self.needLoadDatas.count > 0 && [self.needLoadDatas indexOfObject:indexPath] == NSNotFound) {
    //TODO:清理工作
    return;
}

GPU層面優化

1.避免短時間內大量顯示圖片

盡可能將多張圖片合成一張進行顯示

  • RunLoop小操作
    當前線程是主線程時,某些UI事件,比如ScrollView正在拖動,將會RunLoop切換成NSEventTrackingRunLoopMode模式,在這個模式下默認的NSDefaultRunLoopMode模式中注冊的事件是不會執行
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    
    KJTestModel *model = self.datas[indexPath.row];
    if (model.iconImage) {
        cell.imageView.image = model.iconImage;
    }else{
        NSDictionary *dict = @{@"imageView":cell.imageView,@"model":model};
        [self performSelector:@selector(kj_loadImageView:) withObject:dict afterDelay:0.0 inModes:@[NSDefaultRunLoopMode]];
    }
    cell.nameLabel.text = model.name;
    cell.IDLabel.hidden = model.remarkName == nil ? YES : NO;
    cell.label.text = model.remarkName;
}
/// 下載圖片,并渲染到cell上顯示
- (void)kj_loadImageView:(NSDictionary*)dict{
    UIImageView *imageView = dict[@"imageView"];
    [imageView sd_setImageWithURL:model.avatar completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
        KJTestModel *model = dict[@"model"];
        model.iconImage = image;
    }];
}

2.控制尺寸

GPU能處理的最大紋理尺寸是4096x4096,超過這個尺寸就會占用CPU資源進行處理,所以紋理盡量不要超過這個尺寸

3.減少圖層混合操作

當多個視圖疊加,放在上面的視圖是半透明的,那么這個時候GPU就要進行混合,把透明的顏色加上放在下面的視圖的顏色混合之后得出一個顏色再顯示在屏幕上,這一步是消耗GPU資源

  • UIView的backgroundColor不要設置為clearColor,最好設置和superView的backgroundColor顏色一樣。
  • 圖片避免使用帶alpha通道的圖片

4.透明處理

減少透明的視圖,不透明的就設置opaque = YES

5.避免離屏渲染

離屏渲染就是在當前屏幕緩沖區以外,新開辟一個緩沖區進行操作
離屏渲染的整個過程,需要多次切換上下文環境,先是從當前屏幕切換到離屏;等到離屏渲染結束以后,將離屏緩沖區的渲染結果顯示到屏幕上,又需要將上下文環境從離屏切換到當前屏幕

1 - 下面的情況或操作會引發離屏渲染

  • 光柵化,layer.shouldRasterize = YES
  • 遮罩,layer.mask
  • 圓角,同時設置 layer.masksToBounds = YES 和 layer.cornerRadius > 0
  • 陰影,layer.shadow
  • layer.allowsGroupOpacity = YES 和 layer.opacity != 1
  • 重寫drawRect方法

2 - 圓角優化

這里主要其實就是解決同時設置layer.masksToBounds = YESlayer.cornerRadius > 0就會產生的離屏渲染
其實我們在使用常規視圖切圓角時,可以只使用view.layer.cornerRadius = 3.0,這時是不會產生離屏渲染
但是UIImageView這家伙有點特殊,切圓角時必須上面2句同時設置,則會產生離屏渲染,所以我們考慮通過 CoreGraphics 繪制裁剪圓角,或者叫美工提供圓角圖片

- (UIImage *)kj_ellipseImage{
    UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
    CGContextAddEllipseInRect(ctx, rect);
    CGContextClip(ctx);
    [self drawInRect:rect];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

鏤空圓形圖片覆蓋,此方法可以實現圓形頭像效果,這個也是極為高效的方法。缺點就是對視圖的背景有要求,單色背景效果就最為理想

3 - 陰影優化

對于shadow,如果圖層是個簡單的幾何圖形或者圓角圖形,我們可以通過設置shadowPath來優化性能,能大幅提高性能

imageView.layer.shadowColor = [UIColor grayColor].CGColor;
imageView.layer.shadowOpacity = 1.0;
imageView.layer.shadowRadius = 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
imageView.layer.shadowPath = path.CGPath;

4 - 強制開啟光柵化

當圖像混合了多個圖層,每次移動時,每一幀都要重新合成這些圖層,十分消耗性能,這時就可以選擇強制開啟光柵化layer.shouldRasterize = YES
當我們開啟光柵化后,會在首次產生一個位圖緩存,當再次使用時候就會復用這個緩存,但是如果圖層發生改變的時候就會重新產生位圖緩存。
所以這個功能一般不能用于UITableViewCell中,復用反而降低了性能。最好用于圖層較多的靜態內容的圖形

5 - 優化建議

  • 使用中間透明圖片蒙上去達到圓角效果
  • 使用ShadowPath指定layer陰影效果路徑
  • 使用異步進行layer渲染
  • 將UITableViewCell及其子視圖的opaque屬性設為YES,減少復雜圖層合成
  • 盡量使用不包含透明alpha通道的圖片資源
  • 盡量設置layer的大小值為整形值
  • 背景色的alpha值應該為1,例如不要使用clearColor
  • 直接讓美工把圖片切成圓角進行顯示,這是效率最高的一種方案
  • 很多情況下用戶上傳圖片進行顯示,可以讓服務端處理圓角

最后簡單介紹TableViewCell的部分常用屬性

功能 API & Property
設置分割線顏色 [tableView setSeparatorColor:UIColor.orangeColor]
設置分割線樣式 tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine
是否允許多選 tableView.allowsMultipleSelection
是否響應點擊操作 tableView.allowsSelection = YES
返回選中的多行 tableView.indexPathsForSelectedRows
可見的行 tableView.indexPathsForVisibleRows

UITableView性能優化介紹就到此完畢,后面有相關再補充,寫文章不容易,還請點個小星星傳送門

備注:本文用到的部分函數方法和Demo,均來自三方庫KJEmitterView,如有需要的朋友可自行pod 'KJEmitterView'引入即可

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 在iOS應用中,UITableView應該是使用率最高的視圖之一了。iPod、時鐘、日歷、備忘錄、Mail、天氣、...
    劉光軍_MVP閱讀 3,784評論 5 15
  • 1.最常用的就是cell的重用, 注冊重用標識符它的原理是,根據cell高度和tableView大小,確定界面上能...
    樹下敲代碼的超人閱讀 4,914評論 4 26
  • Swift1> Swift和OC的區別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,135評論 1 32
  • 在iOS應用中,UITableView應該是使用率最高的視圖之一了。iPod、時鐘、日歷、備忘錄、Mail、天氣、...
    baihualinxin閱讀 215評論 0 0
  • UITableView性能優化1.使用不透明視圖:不透明的視圖可以極大地提高渲染的速度。因此如非必要,可以將tab...
    問題餓閱讀 287評論 0 1