iOS tableView 優化

  1. tableview 性能優化方法總覽
  2. tableViewCell 複用
  3. 緩存 cell 高度
  4. 圓角優化
  5. 異步繪製
  6. 其餘優化
  7. 一些優化方案的對比

tableview 性能優化方法總覽

  1. tableview懶加載、cell重用
  2. 高度緩存(由於 heightForRowAtIndexPath: 是調用最頻繁的方法)
    • 當 cell 的行高固定時,使用固定行高 self.tableView.rowHeight = 88;
    • 當 cell 的行高是不固定時,根據內容進行計算後緩存起來使用。第一次確定會計算,後續使用緩存時就避免了屢次計算;高度的計算方法一般寫在自定義的cell中,調用時,既能夠在設置 cell 高的代理方法中使用,也能夠自定義的 model 中使用(且使用時,使用get方法處理);
  3. 數據處理
    (1)使用正確的數據結構來存儲數據;
    (2)數據儘可能採用局部的 section,或 cellRow 的刷新,避免 reloadData;
    (3)大量數據操做時,使用異步子線程處理,避免主線程中直接操做;
    (4)緩存請求結果;
  4. 異步加載圖片:SDWebImage 的使用
    (1)使用異步子線程處理,而後再返回主線程操做;
    (2)圖片緩存處理,避免屢次處理操做;
    (3)圖片圓角處理時,設置 layer 的 shouldRasterize 屬性爲 YES,能夠將負載轉移給 CPU;
  5. 按需加載內容
    (1)滑動操做時,只顯示目標範圍內的cell內容,顯示過的超出目標範圍內以後則進行清除;
    (2)滑動過程當中,不加載顯示圖片,中止時才加載顯示圖片;
  6. 視圖層面
    (1)減小 subviews 的數量,自定義的子視圖能夠整合在造成一個總體的就整合成一個總體的子視圖;
    (2)使用 drawRect 進行繪製(即將 GPU 的部分渲染轉接給 CPU ),或 CALayer 進行文本或圖片的繪製。在實現 drawRect 方法的時候注意減小多餘的繪製操做,它的參數 rect 就是咱們須要繪製的區域,在 rect 範圍以外的區域咱們不須要進行繪製,不然會消耗至關大的資源。
    (3)異步繪製,且設置屬性 self.layer.drawsAsynchronously = YES;(遇到複雜界面,遇到性能瓶頸時,可能就是突破口);
    (4)定義一種(儘可能少)類型的 Cell 及善用 hidden 隱藏(顯示) subviews
    (5)儘可能使全部的 view 的 opaque 屬性爲 YES,包括 cell 自身,以提升視圖渲染速度(避免無用的 alpha 通道合成,下降 GPU 負載)
    (6)避免漸變,圖片縮放的操做
    (7)使用 shadowPath 來畫陰影
    (8)儘可能不使用 cellForRowAtIndexPath: ,若是你須要用到它,只用一次而後緩存結果
    (9)cellForRowAtIndexPath 不要作耗時操做:如不讀取文件 / 寫入文件;儘可能少用 addView 給 Cell 動態添加 View,能夠初始化時就添加,而後經過 hide 來控制是否顯示
    (10)咱們在 cell 上添加系統控件的時候,實際上系統都會調用底層的接口進行繪製,大量添加控件時,會消耗很大的資源而且也會影響渲染的性能。當使用默認的 UITableViewCell 而且在它的 ContentView 上面添加控件時會至關消耗性能。因此目前最佳的方法仍是繼承 UITableViewCell,並重寫 drawRect 方法。
    (11)當咱們須要圓角效果時,可使用一張中間透明圖片蒙上去使用 ShadowPath 指定 layer 陰影效果路徑使用異步進行 layer 渲染(Facebook 開源的異步繪製框架 AsyncDisplayKit )設置 layer 的 opaque 值爲 YES ,減小複雜圖層合成儘可能使用不包含透明(alpha)通道的圖片資源儘可能設置 layer 的大小值爲整形值直接讓美工把圖片切成圓角進行顯示,這是效率最高的一種方案不少狀況下用戶上傳圖片進行顯示,可讓服務端處理圓角使用代碼手動生成圓角 Image 設置到要顯示的 View 上,利用 UIBezierPath ( CoreGraphics 框架)畫出來圓角圖片

tableViewCell 複用

  1. tableViewCell複用介紹 tableView 內部有一個 cell池,裏面放的就是你以前建立過的 cell 。內存豐富時會保存一些 UITableViewCell 對象放入到 cell 池,在須要調用的時候迅速的返回,而不用建立。內存吃緊時 cell 池會自動清理一些多餘的 UITableViewCell 對象。至於有多少 cell ,這個內部會自動控制。
    注意:重取出來的 cell 是有可能捆綁過數據或者加過子視圖的,因此,若是有必要,要清除數據(如 label 的邊框),從而使其顯示正確的內容。html

  2. tableviewCell 複用的方法
    dequeueReusableCellWithIdentifier:forIndexPath: (iOS6引入)ios

    // 必須與register方法配套使用,不然返回的cell可能爲nil,會crash
    [slef.myTableView registerClass:[MyCell class] forCellReuseIdentifier:NSStringFromClass([MyCell class])];
    MyCell* cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([MyCell class]) forIndexPath:indexPath];
    複製代碼
  3. 註冊不一樣類型的 cell 或者不復用,此處以不復用爲例git

    @property (nonatomic, strong) NSMutableDictionary *cellDic;//放cell的標識符
    
    // 每次先從字典中根據IndexPath取出惟一標識符
    NSString *identifier = [_cellDic objectForKey:[NSString stringWithFormat:@"%@", indexPath]];
    // 若是取出的惟一標示符不存在,則初始化惟一標示符,並將其存入字典中,對應惟一標示符註冊Cell
    if (identifier == nil) {
       identifier = [NSString stringWithFormat:@"%@%@", @"cell", [NSString stringWithFormat:@"%@", indexPath]];
       [_cellDic setValue:identifier forKey:[NSString stringWithFormat:@"%@", indexPath]];
       // 註冊Cell
       [self.tableview registerClass:[MyCell class]  forCellWithReuseIdentifier:identifier];
    }
        
    MyCell *cell = [tableView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
    複製代碼
  4. UITableView 複用機制原理:
    查看UITableView頭文件,會找到NSMutableArray *visiableCells,和NSMutableDictionary *reusableTableCells兩個結構。其中visiableCells用來存儲當前UITableView顯示的cell,reusableTableCells用來存儲已經用'identify'緩存的cell。當UITableView滾動的時候,會先在reusableTableCells中根據identify找是否有有已經緩存的cell,若是有直接用,沒有再去初始化。(TableView顯示之初,reusableTableCells爲空,那麼tableView dequeueReusableCellWithIdentifier:CellIdentifier返回nil。開始的cell都是經過[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]來建立,並且cellForRowAtIndexPath只是調用最大顯示cell數的次數)github

緩存 cell 高度

  1. 若是用的 frame ,則給 model 添加一個 cellH 的屬性,而後在獲取數據時計算好高度賦值給 cellH。
  2. 若是用的 AutoLayout,建立相應佈局等同的 cell,計算好高度而後緩存。
    @property (nonatomic, strong) NSMutableDictionary *heightAtIndexPath;//緩存高度所用字典
     #pragma mark - UITableViewDelegate-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
     {    
         NSNumber *height = [self.heightAtIndexPath objectForKey:indexPath];  if(height){       
             return height.floatValue;
         }else {        
             return 100;
         }
     }
    
     - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
     {    NSNumber *height = @(cell.frame.size.height);
         [self.heightAtIndexPath setObject:height forKey:indexPath];
     }
    複製代碼

FD 的實現:fd_heightForCellWithIdentifier: configuration: 方法會根據 identifier 以及 configuration block 提供一個和 cell 佈局相同的 template layout cell,並將其傳入 fd_systemFittingHeightForConfiguratedCell: 這個私有方法返回計算出的高度。主要使用技術爲 runtime 。

離屏渲染

OpenGL中,GPU屏幕渲染有如下兩種方式: On-Screen Rendering:意思是當前屏幕渲染,指的是GPU的渲染操做是在當前用於顯示的屏幕緩衝區進行。 Off-Screen Rendering:意思就是咱們說的離屏渲染了,指的是GPU在當前屏幕緩衝區之外新開闢一個緩衝區進行渲染操做。緩存

相比於當前屏幕渲染,離屏渲染的代價是很高的,主要體如今兩個方面:性能優化

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

離屏渲染觸發條件bash

  1. custom drawRect: (any, even if you simply fill the background with color)
  2. CALayer mask
  3. CALayer shadow
  4. any custom drawing using CGContext 具體表現爲mask(遮罩)、 shadow(陰影)、shouldRasterize(光柵化)、edge antialiasing(抗鋸齒)、group opacity(不透明)、複雜形狀設置圓角等、漸變
  • CPU和GPU: CPU,負責視圖相關的計算工做並告知GPU應該怎麼繪圖; GPU,進行圖形的繪製、渲染等工做;

圓角優化

  • 優化方案1:使用貝塞爾曲線UIBezierPath和Core Graphics框架畫出一個圓角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"]; 
//開始對imageView進行畫圖 
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0); 
//使用貝塞爾曲線畫出一個圓形圖
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
[imageView drawRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext(); 
//結束畫圖 
UIGraphicsEndImageContext();
[self.view addSubview:imageView]; 
複製代碼
  • 優化方案2:使用CAShapeLayer和UIBezierPath設置圓角
UIImageView *imageView = [[UIImageViewalloc]initWithFrame:CGRectMake(100,100,100,100)];
imageView.image = [UIImageimageNamed:@"myImg"];
UIBezierPath *maskPath = [UIBezierPathbezierPathWithRoundedRect:imageView.boundsbyRoundingCorners:UIRectCornerAllCornerscornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
//設置大小
maskLayer.frame=imageView.bounds;
//設置圖形樣子
maskLayer.path=maskPath.CGPath;
imageView.layer.mask=maskLayer;
[self.viewaddSubview:imageView];
複製代碼

對於方案2須要解釋的是: CAShapeLayer繼承於CALayer,可使用CALayer的全部屬性值;CAShapeLayer須要貝塞爾曲線配合使用纔有意義(也就是說纔有效果)使用CAShapeLayer(屬於CoreAnimation)與貝塞爾曲線能夠實現不在view的drawRect(繼承於CoreGraphics走的是CPU,消耗的性能較大)方法中畫出一些想要的圖形CAShapeLayer動畫渲染直接提交到手機的GPU當中,相較於view的drawRect方法使用CPU渲染而言,其效率極高,能大大優化內存使用狀況。 總的來講就是用CAShapeLayer的內存消耗少,渲染速度快,建議使用優化方案2。數據結構

異步繪製

系統繪製流程圖

  1. CALayer內部建立一個backing store(CGContextRef)();
  2. 判斷layer是否有代理; 有代理:調用delegete的drawLayer:inContext, 而後在合適的實際回調代理, 在[UIView drawRect]中作一些繪製工做; 沒有代理:調用layer的drawInContext方法,
  3. layer上傳backingStore到GPU, 結束系統的繪製流程;

UIView的繪製流程圖

  1. UIView調用setNeedsDisplay,可是沒當即進行視圖的繪製工做;
  2. UIView調用setNeedDisplay後,系統調用view對應layer的 setNeedsDisplay方法;
  3. 當前runloop即將結束的時候調用CALayer的display方法;
  4. runloop即將結束, 開始視圖的繪製流程;

異步繪製

  1. 異步繪製的入口在[layer.delegate displayLayer]
  2. 異步繪製過程當中代理負責生成對應的位圖(bitmap);
  3. 將bitmap賦值給layer.content屬性;

  1. 某個時機調用setNeedsDisplay;
  2. runloop將要結束的時候調用[CALayer display]
  3. 若是代理實現了dispalyLayer將會調用此方法, 在子線程中去作異步繪製的工做;
  4. 子線程中作的工做:建立上下文, 控件的繪製, 生成圖片;
  5. 轉到主線程, 設置layer.contents, 將生成的視圖展現在layer上面;
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN
@interface AsyncLabel : UIView
@property (nonatomic, copy)     NSString    *asynText;
@property (nonatomic, strong)   UIFont      *asynFont;
@property (nonatomic, strong)   UIColor     *asynBGColor;
@end
NS_ASSUME_NONNULL_END


#import "AsyncLabel.h"
#import <CoreText/CoreText.h>

@implementation AsyncLabel

- (void)displayLayer:(CALayer *)layer {
    /**
     除了在drawRect方法中, 其餘地方獲取context須要本身建立[https://www.jianshu.com/p/86f025f06d62]
     coreText用法簡介:[https://www.cnblogs.com/purple-sweet-pottoes/p/5109413.html]
     */
      CGSize size = self.bounds.size;;
      CGFloat scale = [UIScreen mainScreen].scale;
    ///異步繪製:切換至子線程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        UIGraphicsBeginImageContextWithOptions(size, NO, scale);
        ///獲取當前上下文
        CGContextRef context = UIGraphicsGetCurrentContext();
        ///將座標系反轉
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        ///文本沿着Y軸移動
        CGContextTranslateCTM(context, 0, size.height);
        ///文本反轉成context座標系
        CGContextScaleCTM(context, 1.0, -1.0);
        ///建立繪製區域
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
        ///建立須要繪製的文字
        NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:self.asynText];
        [attStr addAttribute:NSFontAttributeName value:self.asynFont range:NSMakeRange(0, self.asynText.length)];
        [attStr addAttribute:NSBackgroundColorAttributeName value:self.asynBGColor range:NSMakeRange(0, self.asynText.length)];
        ///根據attStr生成CTFramesetterRef
        CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attStr);
        CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attStr.length), path, NULL);
        ///將frame的內容繪製到content中
        CTFrameDraw(frame, context);
        UIImage *getImg = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        ///子線程完成工做, 切換到主線程展現
        dispatch_async(dispatch_get_main_queue(), ^{
            self.layer.contents = (__bridge id)getImg.CGImage;
        });
    });
}

@end



#import "ViewController.h"
#import "AsyncLabel.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    AsyncLabel *asLabel = [[AsyncLabel alloc] initWithFrame:CGRectMake(50, 100, 200, 200)];
    asLabel.backgroundColor = [UIColor cyanColor];
    asLabel.asynBGColor = [UIColor greenColor];
    asLabel.asynFont = [UIFont systemFontOfSize:16 weight:20];
    asLabel.asynText = @"學習異步繪製相關知識點, 學習異步繪製相關知識點";
    [self.view addSubview:asLabel];
    ///不調用的話不會觸發 displayLayer方法
    [asLabel.layer setNeedsDisplay];
}

@end
複製代碼

其餘優化

  • 子線程異步處理數據
- (void)loadData{   
 // 開闢子線程處理數據    
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{        
    // 處理數據        coding...        
    // 返回主線程處理        
    dispatch_async(dispatch_get_main_queue(), ^{            
        [self.mainTableView reloadData];       
    });    
});
複製代碼
  • 不要作多餘的繪製工做
    在實現drawRect:的時候,它的rect參數就是須要繪製的區域,這個區域以外的不須要進行繪製。 例如上例中,就能夠用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判斷是否須要繪製image和text,而後再調用繪製方法。框架

  • 如圖,這個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;
    }
    複製代碼
    固然這不是減小對象的建立,而是減小了計算的次數,減小了頻繁調用方法裏的邏輯,從而達到更快的速度。

一些優化方案對比

  1. Autolayout + AutomaticDimension:
    由AutoLayout進行佈局,Cell自適應的高度使用系統UITableViewAutomaticDimension。這種實現方案用起來簡單,但當TableView快速滑動時,就會出現掉幀,特別卡。
  2. Autolayout + CountHeight:
    AutoLayout的方式佈局,Cell的高度放在子線本身計算的,優於第一種實現方式,不過掉幀也是比較嚴重的。
  3. FrameLayout + CountHeight:
    Frame佈局,Cell高度在子線程中進行計算步。比較流暢的,折中方案。
  4. YYKit + CountHeight:
    用到了YYKit中的控件,而且使用Frame佈局與Cell高度的計算。這種方式要優於上面的解決方案,由於YYKit中的一些控件作了優化。
  5. AsyncDisplayKit + CountHeight:
    使用了AsyncDisplayKit中提供的相關Note代替系統的原生控件,這種實現方式是這5種實現方式中最爲流暢的。

文章推薦:
VVeboTableViewDemo
性能優化-UITableView的優化使用
iOS 保持界面流暢的技巧

相關文章
相關標籤/搜索