实战UITableview深度优化
演示项目下载地址: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的流程:
- 获取数据; 
- 把数据转化成model、存进数组; 
- tableview调用reloadData刷新数据; 
- 在代理方法cellForRowAtIndexPath里,创建自定义的cell,把model赋值给cell; 
- cell在对应的model的set方法里,根据拿到的model,设置图片的image,设置label的text等(控件都以懒加载形式初始化); 
- 在代理方法heightForRowAtIndexPath里,根据model,算出当前行应该显示多少的高度; 
- 在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) {
    }];
总之是能在异步操作的,都异步操作。
通常来说,UIKit和CoreAnimation相关操作必须在主线程中进行,其它的可以在后台线程异步执行。比方说图像的异步绘制等,具体的后面介绍。
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 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。
所以在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);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
- 离屏渲染触发条件 
--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深度优化的更多相关文章
- 深度优化LNMP之Nginx [2]
		深度优化LNMP之Nginx [2] 配置Nginx gzip 压缩实现性能优化 1.Nginx gzip压缩功能介绍 Nginx gzuo压缩模块提供了压缩文件内容的功能,用户请求 ... 
- Reading | 《TensorFlow:实战Google深度学习框架》
		目录 三.TensorFlow入门 1. TensorFlow计算模型--计算图 I. 计算图的概念 II. 计算图的使用 2.TensorFlow数据类型--张量 I. 张量的概念 II. 张量的使 ... 
- 人工智能深度学习框架MXNet实战:深度神经网络的交通标志识别训练
		人工智能深度学习框架MXNet实战:深度神经网络的交通标志识别训练 MXNet 是一个轻量级.可移植.灵活的分布式深度学习框架,2017 年 1 月 23 日,该项目进入 Apache 基金会,成为 ... 
- TensorFlow+实战Google深度学习框架学习笔记(5)----神经网络训练步骤
		一.TensorFlow实战Google深度学习框架学习 1.步骤: 1.定义神经网络的结构和前向传播的输出结果. 2.定义损失函数以及选择反向传播优化的算法. 3.生成会话(session)并且在训 ... 
- ASP.NET WebApi 文档Swagger深度优化
		本文版权归博客园和作者吴双本人共同所有,转载和爬虫请注明博客园蜗牛原文地址,cnblogs.com/tdws 写在前面 请原谅我这个标题党,写到了第100篇随笔,说是深度优化,其实也并没有什么深度 ... 
- MySQL内核深度优化
		版权声明:本文由简怀兵原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/179 来源:腾云阁 https://www.qclo ... 
- 深度优化LNMP之Nginx (转)
		深度优化LNMP之Nginx Nginx基本安全优化 1.调整参数隐藏Nginx版本号信息 一般来说,软件的漏洞都和版本有关,因此我们应尽量隐藏或清除Web服务队访问的用户显示各类敏感信息(例 ... 
- 深度优化LNMP之PHP (转)
		深度优化LNMP之PHP PHP缓存加速介绍 1.操作码介绍及缓存原理 当客户端请求一个php程序时,服务器的PHP引擎会解析该PHP程序,并将其编译为特定的操作码文件(Operate ... 
- 腾讯云数据库团队:浅谈如何对MySQL内核进行深度优化
		作者介绍:简怀兵,腾讯云数据库团队高级工程师,负责腾讯云CDB内核及基础设施建设:先后供职于Thomson Reuters和YY等公司,PTimeDB作者,曾获一项发明专利:从事MySQL内核开发工作 ... 
随机推荐
- 爬虫之xpath用法
			导包用: from lxml import etree 
- es6的模块化--AMD/CMD/commonJS/ES6
			, ); ); }) , )); }); , )); ; export { firstName, lastName, year }; // es6引用 import { firstName, last ... 
- 使用PHPStorm 配置自定义的Apache与PHP环境
			使用PHPStorm 配置自定义的Apache与PHP环境之一 关于phpstorm配置php开发环境,大多数资料都是直接推荐安装wapmserver.而对于如何配置自定义的PHP环境和Apach ... 
- Python并发复习3 - 多进程模块 multiprocessing
			python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程.Python提供了非常好用的多进程包multiprocessing,只需要定 ... 
- 输出日文CSV乱码问题
			直接写用Excel打开时会乱码,需要加上下面代码中注释的三行 fos = new FileOutputStream(file, false); //fos.write( 0xef ); //fos.w ... 
- [C程序设计基础]快速排序
			//从大到小排序 ///三个参数 a要排序的 数组, l扫左边的 r扫右边 void quickSort(int a[],int l, int r){ /// 左边要小于 右边才有意义 if (l & ... 
- IconFont 图标制作和使用
			一.制作:IcoMoon 这个教程一搜一大把,是很方便快捷的一种方式,提供上传.编辑或者选择IcoMoon-Free下载可以直接拿来用了. 网址:https://icomoon.io/app/ 上传需 ... 
- Linux下MySQL数据库常用基本操作
			1.显示数据库 show databases; 2.选择数据库 use 数据库名; 3.显示数据库中的表 show tables; 4.显示数据表的结构 describe 表名; 5.显示表中记录 S ... 
- [CC-MCO16306]Fluffy and Alternating Subsequence
			[CC-MCO16306]Fluffy and Alternating Subsequence 题目大意: 给定一个\(1\sim n(n\le3\times10^5)\)的排列\(a\). 对于一个 ... 
- vim技巧4 删除/保留文本中匹配行
			vim技巧:如何删除/保留文本中特定的行呢? <ol><a href="/ss/ss/www"> show invisibles</a> < ... 
