[离屏渲染]
一、Off-Screen Rendering#
离屏渲染,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
离屏渲染的数量才是影响 app 交互性能的根源。
离屏渲染耗时是发生在离屏这个动作上面,而不是渲染。原因主要在于创建缓冲区和上下文切换。创建新的缓冲区代价都不算大,付出最大代价的是上下文切换。
二、上下文切换#
不管是在 GPU 渲染过程中,还是熟悉的进程切换,上下文切换在哪里都是一个相当耗时的操作。
首先要保存当前屏幕渲染环境,然后切换到一个新的绘制环境,申请绘制资源 -> 初始化环境 -> 开始一个绘制 -> 绘制完毕后销毁这个绘制环境,如需要切换到 On-Screen Rendering 或者再开始一个新的离屏渲染重复之前的操作。
下图描述了一次 mask 的渲染操作。
<center style="margin: 0px; padding: 0px;"></center>
一次 mask 发生了两次离屏渲染和一次主屏渲染。即使忽略昂贵的上下文切换,一次 mask 需要渲染三次才能在屏幕上显示,这已经是普通视图显示 3 倍耗时,若再加上下文环境切换,一次 mask 就是普通渲染的 30 倍以上耗时操作。
三、出现场景#
下面的情况或操作会引发离屏渲染:
- 为图层设置遮罩(layer.mask)
- 将图层的 layer.masksToBounds/view.clipsToBounds 属性设置为 true
- 将图层的 layer.allowsGroupOpacity 属性设置为 YES 和 layer.opacity < 1.0
- 为图层设置阴影(layer.shadow*)
- 为图层设置 layer.shouldRasterize = true
- 具有 layer.cornerRadius、layer.edgeAntialiasingMask、layer.allowsEdgeAntialiasing 的图层
- 文本(任何种类,包括 UILabel、CATextLayer、CoreText 等)
- 使用 CGContext 在 drawRect: 方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现
四、优化方案#
官方对离屏渲染产生性能问题也进行了优化:iOS 9.0 之前 UIImageView 跟 UIButton 设置圆角都会触发离屏渲染;iOS 9.0 之后 UIButton 设置圆角会触发离屏渲染,而 UIImageView 设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。
4.1 圆角优化#
在 APP 开发中圆角图片还是经常出现的。如果一个界面中只有少量圆角图片或许对性能没有非常大的影响,但是当圆角图片比较多的时候就会 APP 性能产生明显的影响。
我们设置圆角一般通过如下方式:
<pre style="margin: 10px 0px; padding: 0px; white-space: pre !important; overflow-wrap: break-word; position: relative !important;">
Copy
imageView.layer.cornerRadius = CGFloat(10); imageView.layer.masksToBounds = YES;
</pre>
这样处理的渲染机制是 GPU 在当前屏幕缓冲区外新开辟一个渲染缓冲区进行工作,也就是离屏渲染,这会带来额外的性能损耗,如果这样的圆角操作达到一定数量,会触发缓冲区的频繁合并和上下文的的频繁切换,出现掉帧现象。
-
使用贝塞尔曲线 UIBezierPath 和 Core Graphics 框架画出一个圆角。
<pre style="margin: 10px 0px; padding: 0px; white-space: pre !important; overflow-wrap: break-word; position: relative !important;">
Copy
`UIImageView * imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
UIImage * image = [UIImage imageNamed:@"myImg"];// 开始对 imageView 进行画图
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, [UIScreen mainScreen].scale);
// 使用贝塞尔曲线画出一个圆形图
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
[image drawInRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
// 结束画图
UIGraphicsEndImageContext();[self.view addSubview:imageView];`</pre>
-
视图上添加一个子 layer 到最上层,用于遮盖该视图及其子视图,设置 layer 的图片为刚好能够遮盖成所需圆角样子,并且图片颜色刚好是该视图父视图的背景颜色就达到想要的效果。
弊端:如果该父视图的颜色不是纯色,此时该方式就不适用了,同样,如果父视图的颜色会变化,那实现起来的代码也不那么优雅。
<center style="margin: 0px; padding: 0px;">
[图片上传失败...(image-fc9a5a-1583574576970)]
</center> -
通过修改 layer.mask,首先通过贝塞尔曲线创建基于矢量的路径,传递给 CAShapeLayer 进行渲染。路径闭环,再把绘制出的 Shape 赋值给 layer.mask,在 Mask 范围之外的 Layer 将不被显示从而达到圆角效果。代码实现很简单,如下:
<pre style="margin: 10px 0px; padding: 0px; white-space: pre !important; overflow-wrap: break-word; position: relative !important;">
Copy
UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(130, 330, 100, 100)]; [btn setBackgroundColor:[UIColor colorWithRed:(226.0 / 255.0) green:(113.0 / 255.0) blue:(19.0 / 255.0) alpha:1]]; [backgroundImageView addSubview:btn]; //绘制曲线路径 UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:btn.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:btn.bounds.size]; CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init]; //设置大小 maskLayer.frame = btn.bounds; //设置图形样子 maskLayer.path = maskPath.CGPath; btn.layer.mask = maskLayer;
</pre><center style="margin: 0px; padding: 0px;">
[图片上传失败...(image-54fb3-1583574576970)]
</center>
4.2 shadow 优化#
对于 shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置 shadowPath 来优化性能,能大幅提高性能。示例如下:
<pre style="margin: 10px 0px; padding: 0px; white-space: pre !important; overflow-wrap: break-word; position: relative !important;">
Copy
imageView.layer.shadowColor = [UIColor grayColor].CGColor; imageView.layer.shadowOpacity = 1.0; imageView.layer.shadowRadius = 2.0; UIBezierPath * path = [UIBezierPath bezierPathWithRect:imageView.frame]; imageView.layer.shadowPath = path.CGPath;
</pre>
当使用阴影的视图形状发生变化时,即 shadowPath 并不会跟随 CALayer 的 bounds 属性进行变化,所以在 layer 的 bounds 产生变化以后需要手动更新 shadowPath 才能让其适配新的 bounds。具体推荐看这篇文章
我们还可以通过设置 shouldRasterize 属性值为 YES 来强制开启离屏渲染。其实就是光栅化(Rasterization)。既然离屏渲染这么不好,为什么我们还要强制开启呢?当一个图像混合了多个图层,每次移动时,每一帧都要重新合成这些图层,十分消耗性能。当我们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存。但是如果图层发生改变的时候就会重新产生位图缓存。所以这个功能一般不能用于 UITableViewCell 中,cell 的复用反而降低了性能。最好用于图层较多的静态内容的图形。而且产生的位图缓存的大小是有限制的,一般是 2.5 个屏幕尺寸。在 100ms 之内不使用这个缓存,缓存也会被删除。所以我们要根据使用场景而定。
4.3 其他的一些优化建议#
- 当我们需要圆角效果时,可以使用一张中间透明图片蒙上去
- 使用 ShadowPath 指定 layer 阴影效果路径
- 使用异步进行 layer 渲染(Facebook 开源的异步绘制框架 AsyncDisplayKit)
- 设置 layer 的 opaque 值为 YES,减少复杂图层合成
- 尽量使用不包含透明(alpha)通道的图片资源
- 尽量设置 layer 的大小值为整形值
- 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
- 很多情况下用户上传图片进行显示,可以让服务端处理圆角
- 使用代码手动生成圆角 Image 设置到要显示的 View 上,利用 UIBezierPath(CoreGraphics框架)画出来圆角图片
4.4 Core Animation 工具检测离屏渲染#
对于离屏渲染的检测,苹果为我们提供了一个测试工具Core Animation。可以在Xcode->Open Develeper Tools->Instruments中找到,如下图:
<center style="margin: 0px; padding: 0px;"></center>
Core Animation工具用来监测Core Animation性能,提供可见的FPS值,并且提供几个选项来测量渲染性能。如下图:
<center style="margin: 0px; padding: 0px;"></center>
下面我们来说明每个选项的功能:
Color Blended Layers:这个选项如果勾选,你能看到哪个 layer 是透明的,GPU 正在做混合计算。显示红色的就是透明的,绿色就是不透明的。
Color Hits Green and Misses Red:如果勾选这个选项,且当我们代码中有设置shouldRasterize为YES,那么红色代表没有复用离屏渲染的缓存,绿色则表示复用了缓存。我们当然希望能够复用。
Color Copied Images:按照官方的说法,当图片的颜色格式GPU不支持的时候,Core Animation 会拷贝一份数据让 CPU 进行转化。例如从网络上下载了 TIFF 格式的图片,则需要 CPU 进行转化,这个区域会显示成蓝色。还有一种情况会触发Core Animation的copy方法,就是字节不对齐的时候。
Color Immediately:默认情况下 Core Animation 工具以每毫秒 10 次的频率更新图层调试颜色,如果勾选这个选项则移除 10ms 的延迟。对某些情况需要这样,但是有可能影响正常帧数的测试。
Color Misaligned Images:勾选此项,如果图片需要缩放则标记为黄色,如果没有像素对齐则标记为紫色。像素对齐我们已经在上面有所介绍。
Color Offscreen-Rendered Yellow:用来检测离屏渲染的,如果显示黄色,表示有离屏渲染。当然还要结合 Color Hits Green and Misses Red 来看,是否复用了缓存。
Color OpenGL Fast Path Blue:这个选项对那些使用 OpenGL 的图层才有用,像是 GLKView 或者 CAEAGLLayer,如果不显示蓝色则表示使用了 CPU 渲染,绘制在了屏幕外,显示蓝色表示正常。
Flash Updated Regions:当对图层重绘的时候回显示黄色,如果频繁发生则会影响性能。可以用增加缓存来增强性能。
五、UITableView 优化#
-
使用tableView的复用机制
作用:减少内存资源的消耗。
注意:cell被重用时,它内部绘制的内容并不会被自动清除,因此你可能需要调用setNeedsDisplayInRect: 或 setNeedsDisplay 方法。
-
提前预估高度
提前计算并缓存好高度(布局),因为 heightForRowAtIndexPath: 是调用最频繁的方法。
-
cell 内部有图片
此时需要异步加载图片,防止卡顿(此时的 SDWebImage 的每个 cell 中都创建一个子线程吗?)但是内部开启的线程过多也会影响主线程的性能
解决办法:
- 在 scrollerView 的代理方法中,didEndDragging,didEndDeceleratiing 方法中,才开始异步加载,其它时刻不进行加载
- 在 didEndDragging,didEndDeceleratiing 方法中实现方法:获取屏幕上显示出来的 cell 的 indexPath 数组,然后通过遍历 indexPath 的数组,在数据源中,如果已经加载了,就不需要再异步加载,反之进行异步加载,然后再cellForRow方法中也进行一次判断:当self.tableView.dragging == NO && self.tableView.decelerating == NO 的时候执行异步加载图片的方法
-
尽量少用 addView 给 Cell 动态添加 View
可以初始化时就添加,相对于一些固定的视图在初始化时就布局好,学会用 hidden 属性来控制是否显示。
-
减少子视图的数目
当 cell 上面的子视图数量过多时,会影响滑动性能,当子视图太多的时候,对适当的视图进行绘制。
-
使用不透明视图
不透明的视图可以极大地提高渲染的速度。因此如非必要,可以将 table cell 及其子视图的 opaque 属性设为 YES(默认值)。
-
预渲染图像和离屏渲染
你会发现即使做到了上述几点,当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是预渲染图像,在bitmap context里先将其画一遍,导出成UIImage对象,然后再绘制到屏幕,详细做法可见《利用预渲染加速iOS设备的图像显示》。
离屏渲染就是在 tableView 中展示多张需要切圆形的图片,此时不要使用 setCornerRadius 的方法,这样耗损性能,用 Core Graphics 绘制圆角,然后返回图片,在 SDWebImage 处理我的分类返回的图片,并进行缓存。
-
UIImage:本地图片加载方式本地图片加载常用方法有两种:
- [UIImage imageNamed:@”xx.png”] 图片多次使用时使用,需要使用此方式加入缓存。
- [[UIImage alloc] initWithContentsOfFile:@”xx.png”] 图片不常使用时,不使用缓存。
-
避免对象创建时过多消耗资源
例如:日期处理,将保持日期对象全局唯一。