阅读 818

iOS视图渲染与性能优化

前言

这是一篇关于iOS的视图渲染流程以及性能优化的文章,源于WWDC视频

正文

一、视图渲染

视图渲染的处理层级图如下:

UIKit是常用的框架,显示、动画都通过CoreAnimation;
CoreAnimation是核心动画,依赖于OpenGL ES做GPU渲染(目前最新的iPhone已经都使用Metal,为了和图文一致,本文后面继续使用OpenGL ES来描述),CoreGraphics做CPU渲染;
最底层的GraphicsHardWare是图形硬件。

视图渲染的整体流程如下:
视图渲染到屏幕上需要CPU和GPU一起协作。App将一部分数据通过CoreGraphics、CoreImage调用CPU进行预处理,最终通过OpenGL ES将数据传送到 GPU,最终显示到屏幕。

二、渲染过程


渲染的具体过程可以用上图来描述:

  • 1、CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等;
  • 2、RenderServer解析提交的子树状态,生成绘制指令;
  • 3、GPU执行绘制指令;
  • 4、显示渲染后的数据;

其中App的Commit流程又可以分为Layout、Display、Prepare、Commit四个步骤。

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)

解码图片;
图片格式转换;

当我们使用UIImage、CGImage时,图片并没有真正解码。iOS会先用一些基础的图像信息创建对象,等到真正使用时再创建bitmap并进行解码。尽量避免使用不支持硬解的图片格式,比如说webp;

4、提交(Commit)

打包layers并发送到渲染server;
递归提交子树的layers;
如果子树太复杂,会消耗很大,对性能造成影响;

尽可能简化viewTree;

当显示一个UIImageView时,Core Animation会创建一个OpenGL ES纹理,并确保在这个图层中的位图被上传到对应的纹理中。当你重写 -drawInContext方法时,Core Animation会请求分配一个纹理,同时确保Core Graphics会将你在-drawInContext中绘制的东西放入到纹理的位图数据中。

三、Tile-Based 渲染

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,因为性能消耗巨大。

60FPS的设备,每帧只有16.67ms的时间进行处理。

4、渲染等待

由于每一帧的顶点和像素处理相对独立,iOS会将CPU处理,顶点处理,像素处理安排在相邻的三帧中。如图,当一个渲染命令提交后,要在当帧之后的第三帧,渲染结果才会显示出来。

5、光栅化

把视图的内容渲染成纹理并缓存,可以通过CALayer的shouldRasterize属性开启光栅化。 注意,光栅化的元素,总大小限制为2.5倍的屏幕。 更新内容时,会启用离屏渲染,所以更新代价较大,只能用于静态内容;而且如果光栅化的元素100ms没有被使用将被移除,故而不常用元素的光栅化并不会优化显示。

6、组透明度

CALayer的allowsGroupOpacity属性,UIView 的alpha属性等同于 CALayer opacity属性。
GroupOpacity=YES时,会先不考虑透明度,等绘制完成所有layer(自身+子layers),再统一计算透明。
假设某个视图A有一个字视图B,他们的alpha都是0.5(根视图是黑色,A和B都是白色),当我们绘制视图的时候:
如果未开启组透明,首先是绘制视图A(0.5白色),然后再绘制视图B,绘制视图B的时候是在父视图0.5白色和根视图0.5黑色的基础上叠加视图B的0.5白色,最终就是0.75白色。

如果开启了组透明,首先是绘制视图A(白色),然后在A的基础上直接绘制视图B(白色),最终再统一计算透明0.5,所以A和B的颜色保持一致。(边界是特意加的,为了区分视图B)

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

iOS对底层渲染做了很好的封装,优秀的开发环境允许我们直接使用UIKit进行视图操作,使用CoreAnimation完成复杂的动画,却不需要知道视图渲染的复杂过程,更不需要知道GPU的Tile-Based架构。
但是我们的不应该满足于API的使用,更应该了解其背后复杂的运行规则以及设计原理。

由于WWDC视频内容较久,里面仍在使用OpenGL ES(这部分现在已经由Metal替代),但是这并不影响我们对整个移动端的视图渲染过程进行学习。

附录

TileBasedArchitectures PDF
TileBasedArchitectures 介绍

文章分类
iOS
文章标签