前言
关于iOS的视图渲染流程,以及性能优化的建议。
源于WWDC视频。
我假设你是一个这样的开发者:
- 了解OpenGL ES;
- 了解view hierarchy;
- 了解instruments;
view hierarchy和instruments网上资料很多,OpenGL ES的你可以看OpenGL ES文集。
视图渲染
UIKit是常用的框架,显示、动画都通过CoreAnimation。
CoreAnimation是核心动画,依赖于OpenGL ES做GPU渲染,CoreGraphics做CPU渲染;
最底层的GraphicsHardWare是图形硬件。
下图是另外一种表现的形式。在屏幕上显示视图,需要CPU和GPU一起协作。一部数据通过CoreGraphics、CoreImage由CPU预处理。最终通过OpenGL ES将数据传送到 GPU,最终显示到屏幕。
CoreImage支持CPU、GPU两种处理模式。
显示逻辑
- 1、CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等;
- 2、RenderServer解析提交的子树状态,生成绘制指令;
- 3、GPU执行绘制指令;
-
4、显示渲染后的数据;
提交流程(以动画为例)
第2步为prepare to commit animation (layoutSubviews,drawRect:);
1、布局(Layout)
调用layoutSubviews方法;
调用addSubview:方法;
会造成CPU和I/O瓶颈;
2、显示(Display)
通过drawRect绘制视图;
绘制string(字符串);
会造成CPU和内存瓶颈;
每个UIView都有CALayer,同时图层有一个像素存储空间,存放视图;调用-setNeedsDisplay的时候,仅会设置图层为dirty。
当渲染系统准备就绪,调用视图的-display方法,同时装配像素存储空间,建立一个CoreGraphics上下文(CGContextRef),将上下文push进上下文堆栈,绘图程序进入对应的内存存储空间。
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10, 10)];
[path addLineToPoint:CGPointMake(20, 20)];
[path closePath];
path.lineWidth = 1;
[[UIColor redColor] setStroke];
[path stroke];
在-drawRect方法中实现如上代码,UIKit会将自动生成的CGContextRef 放入上下文堆栈。
当绘制完成后,视图的像素会被渲染到屏幕上;当下次再次调用视图的-setNeedsDisplay,将会再次调用-drawRect方法。
3、准备提交(Prepare)
解码图片;
图片格式转换;
GPU不支持的某些图片格式,尽量使用GPU能支持的图片格式;
4、提交(Commit)
打包layers并发送到渲染server;
递归提交子树的layers;
如果子树太复杂,会消耗很大,对性能造成影响;
尽可能简化viewTree;
当显示一个UIImageView时,Core Animation会创建一个OpenGL ES纹理,并确保在这个图层中的位图被上传到对应的纹理中。当你重写-drawInContext
方法时,Core Animation会请求分配一个纹理,同时确保Core Graphics会将你在-drawInContext
中绘制的东西放入到纹理的位图数据中。
Tile-Based 渲染
这里有PDF文档
Tiled-Based 渲染是移动设备的主流。整个屏幕会分解成N*Npixels组成的瓦片(Tiles),tiles存储于SoC 缓存(SoC=system on chip,片上系统,是在整块芯片上实现一个复杂系统功能,如intel cpu,整合了集显,内存控制器,cpu运核心,缓存,队列、非核心和I/O控制器)。
几何形状会分解成若干个tiles,对于每一块tile,把必须的几何体提交到OpenGL ES,然后进行渲染(光栅化)。完毕后,将tile的数据发送回cpu。
传送数据是非常消耗性能的,相对来说,多次计算比多次发送数据更加经济高效,但是额外的计算也会产生一些性能损耗。
PS:在移动平台控制帧率在一个合适的水平可以节省电能,会有效的延长电池寿命,同时会相对的提高用户体验。这里有详细的介绍
1、普通的Tile-Based渲染流程
1、CommandBuffer,接受OpenGL ES处理完毕的渲染指令;
2、Tiler,调用顶点着色器,把顶点数据进行分块(Tiling);
3、ParameterBuffer,接受分块完毕的tile和对应的渲染参数;
4、Renderer,调用片元着色器,进行像素渲染;
5、RenderBuffer,存储渲染完毕的像素;
2、离屏渲染 —— 遮罩(Mask)
1、渲染layer的mask纹理,同Tile-Based的基本渲染逻辑;
2、渲染layer的content纹理,同Tile-Based的基本渲染逻辑;
3、Compositing操作,合并1、2的纹理;
3、离屏渲染 ——UIVisiualEffectView
使用UIBlurEffect,应该是尽可能小的view,因为性能消耗巨大。
4、渲染等待
由于每一帧的顶点和像素处理相对独立,iOS会将CPU处理,顶点处理,像素处理安排在相邻的三帧中。如图,当一个渲染命令提交后,要在当帧之后的第三帧,渲染结果才会显示出来。
5、光栅化
把视图的内容渲染成纹理并缓存,可以通过CALayer的shouldRasterize属性开启光栅化。
注意,光栅化的元素,总大小限制为2.5倍的屏幕。
更新内容时,会启用离屏渲染,所以更新代价较大,只能用于静态内容;而且如果光栅化的元素100ms没有被使用将被移除,故而不常用元素的光栅化并不会优化显示。
6、组透明度
CALayer的allowsGroupOpacity属性,UIView 的alpha属性等同于 CALayer opacity属性。GroupOpacity=YES,子 layer 在视觉上的透明度的上限是其父 layer 的opacity。当父视图的layer.opacity != 1.0时,会开启离屏渲染。
layer.opacity == 1.0时,父视图不用管子视图,只需显示当前视图即可。
The default value is read from the boolean UIViewGroupOpacity property in the main bundle’s Info.plist file. If no value is found, the default value is YES for apps linked against the iOS 7 SDK or later and NO for apps linked against an earlier SDK.
为了让子视图与父视图保持同样的透明度,从 iOS 7 以后默认全局开启了这个功能。
性能优化
这个是WWDC推荐的检查项目:
1、帧率一般在多少?
60帧每秒;(TimeProfiler)
2、是否存在CPU和GPU瓶颈? (查看占有率)
更少的使用CPU和GPU可以有效的保存电量;
3、额外的使用CPU来进行渲染?
重写了drawRect会导致CPU渲染;在CPU进行渲染时,GPU大多数情况是处于等待状态;
4、是否存在过多离屏渲染?
越少越好;离屏渲染会导致上下文切换,GPU产生idle;
5、是否渲染过多视图?
视图越少越好;透明度为1的视图更受欢迎;
6、使用奇怪的图片格式和大小?
避免格式转换和调整图片大小;一个图片如果不被GPU支持,那么需要CPU来转换。(Xcode有对PNG图片进行特殊的算法优化)
7、使用昂贵的特效?
理解特效的消耗,同时调整合适的大小;例如前面提到的UIBlurEffect;
8、视图树上不必要的元素?
理解视图树上所有点的必要性,去掉不必要的元素;忘记remove视图是很常见的事情,特别是当View的类比较大的时候。
以上,是8个问题对应的工具。遇到性能问题,先分析、定位问题所在,而不是埋头钻进代码的海洋。
性能优化实例
1、阴影
上面的做法,会导致离屏渲染;下面的做法是正确的做法。
2、圆角
不要使用不必要的mask,可以预处理图片为圆形;或者添加中间为圆形透明的白色背景视图。即使添加额外的视图,会导致额外的计算;但仍然会快一点,因为相对于切换上下文,GPU更擅长渲染。
离屏渲染会导致GPU利用率不到100%,帧率却很低。(切换上下文会产生idle time)
3、工具
使用instruments的CoreAnimation工具来检查离屏渲染,黄色是我们不希望看到的颜色。
使用真机来调试,因为模拟器使用的CALayer是OSX的CALayer,不是iOS的CALayer。如果用模拟器调试,会发现所有的视图都是黄色。
总结
视频中的这一句话,让我对iOS的视图渲染茅塞顿开。
CALayer in CA is two triangles.
文章中关于Tile-Based架构,以及像素显示渲染的理解基于我对OpenGL ES学习以及iOS开发收获。
iOS开发收获很容易找到,但是OpenGL ES相对来说很少。越来越觉得自己花时间去研究是值得的。
Core Animation的核心是OpenGL ES的一个抽象物,CoreAnimation让你直接使用OpenGL ES的功能,却不需要处理OpenGL ES的复杂操作。
可是我仍越过CoreAnimation去学习OpenGL ES。因为我不满足用Apple提供的API拼凑界面。