iOS tableView 优化

7,036 阅读12分钟
  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 的边框),从而使其显示正确的内容。

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

    // 必须与register方法配套使用,否则返回的cell可能为nil,会crash [slef.myTableView registerClass:[MyCell class] forCellReuseIdentifier:NSStringFromClass([MyCell class])]; MyCell* cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([MyCell class]) forIndexPath:indexPath];

  3. 注册不同类型的 cell 或者不复用,此处以不复用为例

    @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数的次数)

缓存 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);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。

离屏渲染触发条件

  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 保持界面流畅的技巧