演示项目下载地址:https://github.com/YYProgrammer/YYTableViewDemo

项目里的低性能版是常规写法实现的tableview,高性能版是做了相关优化后的tableview。

tableView滑动为什么会卡?

我们可以想象这样一个场景:

有一个老师、学生A、学生B、一个画板、一个橱窗。

每一秒钟,老师都要告诉学生A一个题目让他们作画,学生A负责研究这个题目表达的含义,然后告诉学生B应该画什么,学生B收到消息后,在画板上画出对应的画,在这一秒钟结束之时,把画贴到橱窗,供外面的人观看。然后继续下一秒的审题、画画的步骤。

正常情况下,学生A、B都能合同愉快,在规定的时间画好,但有时候,学生A审题太久,或者这一秒的量太多,学生B画得不够快,那么这一秒,甚至下几秒,橱窗里的画会保持上一次的画,直到他们画好下一张。

这里,

学生A就是CPU,负责视图相关的计算工作并告知GPU应该怎么绘图;

学生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、避免主线程阻塞

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, ), ^{
//1、字典转模型
//2、计算每个model的数据,布局参数等。
dispatch_async(dispatch_get_main_queue(), ^{
//3、回到主线程,刷新tableview等
});
});
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { }];

总之是能在异步操作的,都异步操作。

通常来说,UIKitCoreAnimation相关操作必须在主线程中进行,其它的可以在后台线程异步执行。比方说图像的异步绘制等,具体的后面介绍。

2、避免频繁的对象创建

对象的创建会发送内存分配、属性调整等。

所以,首先,尽量用轻量的对象代替重量的对象。比如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;
}

当然这不是减少对象的创建,而是减少了计算的次数,减少了频繁调用方法里的逻辑,从而达到更快的速度。

3、减少对象的属性赋值操作

尤其是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。

4、异步绘制

文本渲染、图像绘制都是比较消耗性能的操作,而UILabel等控件都是在主线程进行的文本绘制。这会对性能产生比较大的影响。

UIKit和CoreAnimation相关操作必须在主线程中进行,其它的可以在后台线程异步执行

怎么来简单理解这句话呢?

比方说:为一个UIImageView设置image,

imageView.image = image;

以上代码必须在主线程进行,但这个image的绘制过程,可以在异步线程做

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, ), ^{
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:];
_carVersionLabel.textColor = BLACK_TEXT_COLOR;
_carVersionLabel.numberOfLines = ;
_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 = -;
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,感谢巨人的肩膀。

5、简化视图结构

GPU在绘制图像前,会把重叠的视图进行混合,视图结构越复杂,这个操作就越耗时,如果存在透明视图,混合过程会更加复杂。

所以,我们可以

  • 尽量避免复杂的图层结构

  • 少使用透明的视图

  • 不透明的视图,设置opaque = YES

  • 或者采用VVeboTableViewDemo的方法,把视图异步绘成一张图

6、减少离屏渲染

  • 什么是离屏渲染?

回到文章开头的那个例子,同学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属性,这就会造成离屏渲染。

关于这种问题的处理,大致有两个思路

1、异步绘制一张圆角的图片来显示;

2、用一个圆角而中空的图来盖住。

演示项目里我选择了使用YYKit里的组件来切割图片的圆角。

其它小tips

  • 1、tableview需要刷新数据时,使用

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

而非

[tableview reloadData];

主要原因在于:

1、刷新更少的行,减少cpu压力;

2、使用YYLabel等异步绘制label时,使用reloadData会把之前的row也重绘一次,会造成“Label闪了一下的感觉”。

  • 2、NSDateFormatter这个对象的相关操作很费时,需要避免频繁的创建和计算

  • 3、对于固定行高的cell

tableview.rowHeight = 50.0;

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

效率更高。

  • 4、Autolayout使用在越复杂的界面,CPU越吃力

实战UITableview深度优化的更多相关文章

  1. 深度优化LNMP之Nginx [2]

    深度优化LNMP之Nginx [2]   配置Nginx gzip 压缩实现性能优化 1.Nginx gzip压缩功能介绍        Nginx gzuo压缩模块提供了压缩文件内容的功能,用户请求 ...

  2. Reading | 《TensorFlow:实战Google深度学习框架》

    目录 三.TensorFlow入门 1. TensorFlow计算模型--计算图 I. 计算图的概念 II. 计算图的使用 2.TensorFlow数据类型--张量 I. 张量的概念 II. 张量的使 ...

  3. 人工智能深度学习框架MXNet实战:深度神经网络的交通标志识别训练

    人工智能深度学习框架MXNet实战:深度神经网络的交通标志识别训练 MXNet 是一个轻量级.可移植.灵活的分布式深度学习框架,2017 年 1 月 23 日,该项目进入 Apache 基金会,成为 ...

  4. TensorFlow+实战Google深度学习框架学习笔记(5)----神经网络训练步骤

    一.TensorFlow实战Google深度学习框架学习 1.步骤: 1.定义神经网络的结构和前向传播的输出结果. 2.定义损失函数以及选择反向传播优化的算法. 3.生成会话(session)并且在训 ...

  5. ASP.NET WebApi 文档Swagger深度优化

    本文版权归博客园和作者吴双本人共同所有,转载和爬虫请注明博客园蜗牛原文地址,cnblogs.com/tdws   写在前面 请原谅我这个标题党,写到了第100篇随笔,说是深度优化,其实也并没有什么深度 ...

  6. MySQL内核深度优化

    版权声明:本文由简怀兵原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/179 来源:腾云阁 https://www.qclo ...

  7. 深度优化LNMP之Nginx (转)

    深度优化LNMP之Nginx Nginx基本安全优化 1.调整参数隐藏Nginx版本号信息     一般来说,软件的漏洞都和版本有关,因此我们应尽量隐藏或清除Web服务队访问的用户显示各类敏感信息(例 ...

  8. 深度优化LNMP之PHP (转)

    深度优化LNMP之PHP   PHP缓存加速介绍   1.操作码介绍及缓存原理     当客户端请求一个php程序时,服务器的PHP引擎会解析该PHP程序,并将其编译为特定的操作码文件(Operate ...

  9. 腾讯云数据库团队:浅谈如何对MySQL内核进行深度优化

    作者介绍:简怀兵,腾讯云数据库团队高级工程师,负责腾讯云CDB内核及基础设施建设:先后供职于Thomson Reuters和YY等公司,PTimeDB作者,曾获一项发明专利:从事MySQL内核开发工作 ...

随机推荐

  1. HDU 4185 Oil Skimming 【最大匹配】

    <题目链接> 题目大意: 给你一张图,图中有 '*' , '.' 两点,现在每次覆盖相邻的两个 '#' ,问最多能够覆盖几次. 解题分析: 无向图二分匹配的模板题,每个'#'点与周围四个方 ...

  2. HDU 2444 二分图判断 (BFS染色)+【匈牙利】

    <题目链接> 题目大意: 有N个人,M组互相认识关系互相认识的两人分别为a,b,将所有人划分为两组,使同一组内任何两人互不认识,之后将两个组中互相认识的人安排在一个房间,如果出现单人的情况 ...

  3. Google Hack搜索技巧

    想了解更多搜索技巧,点击下面网站了解http://exploit-db.com/google-dorks Google Hack的一些整理 这里是google关键字的用法,要设置它为中文,则是 htt ...

  4. CSS3 animation 练习

    css3 的动画让 html 页面变得生机勃勃,但是如何用好动画是一门艺术,接下来我来以一个demo为例,来练习css3 animation. 我们先详细了解一下animation 这个属性. ani ...

  5. kafka告警简单方案

    一.前言 为什么要设计kafka告警方案?现成的监控项目百度一下一大堆,KafkaOffsetMonitor.KafkaManager. Burrow等,具体参考:kafka的消息挤压监控.由于本小组 ...

  6. 利用Fiddler拦截接口请求并篡改数据

    近期在测试一个下单的项目,出于安全角度考虑,测试了一个场景,那就是利用工具对接口进行拦截并篡改数据.将接口一拦截并篡改数据后,发现收货满满.开发默默接受了我的建议,并对代码进行了修改. 对于fiddl ...

  7. angular笔记_4(函数)

    angular.isString();是否字符串 angular.isNumber();是否数字 angular.isArray();是否数组 angular.isDate();是否日期/时间 ang ...

  8. BZOJ.3165.[HEOI2013]Segment(李超线段树)

    BZOJ 洛谷 对于线段,依旧是存斜率即可. 表示精度误差一点都不需要管啊/托腮 就我一个人看成了mod(10^9+1)吗.. //4248kb 892ms #include <cstdio&g ...

  9. GCC卡常

    #pragma GCC optimize("Ofast,no-stack-protector") #pragma GCC optimize("-funsafe-loop- ...

  10. hibernate方法中参数传入数组的查询方法

    public List<T> getByIds(Long[] ids) { return getSession().createQuery(// "FROM User WHERE ...