实战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越吃力

相关文章
相关标签/搜索