虽然对网上大家所说的性能优化手段了解得七七八八,但是总感觉少点了东西。后来细想了一下,原来自己对这些内容只是一知半解,未能很好的深入了解更深层次的东西。今天的文章对iOS的渲染做一次深入的了解,做到知已知彼百战百胜。

图像渲染原理

显示原理

从硬件上来看,CPU会经过一系列的处理,最终输出位图,然后通过总线在合适的时机传到GPU上面。GPU拿到位图会做一些图层的渲染、纹理合成等工作。再把结果放到帧缓冲区中,由视频控制器根据Vsny信号,在指定时间之前,提取帧缓冲区的屏幕显示内容,最终显示到显示器。

iOS 视图渲染流程

咱们回到iOS中,下图为 iOS 中的图形渲染流程。

iOS图形渲染

GPU Driver 是直接和 GPU 交流的代码块,使不同的GPU在下一个层级上显示的更为统一,典型的下一层级有 OpenGL/OpenGL ES.

OpenGL(Open Graphics Library) 是一个提供了 2D 和 3D 图形渲染的 API。OpenGL 和 GPU 密切的工作以提高GPU的能力,并实现硬件加速渲染。

OpenGL 之上扩展出很多东西。在 iOS 上,几乎所有的东西都是通过 Core Animation 绘制出来,然而在 OS X 上,绕过 Core Animation 直接使用 Core Graphics 绘制的情况并不少见。对于一些专门的应用,尤其是游戏,程序可能直接和 OpenGL/OpenGL ES 交流。

再看看另外一张图:

这张图,只关注一点: Core Graphics是利用 CPU 进行绘制的。通常我们所进行的异步绘制就是在这样,利用 Core Graphics绘制一张图片,然后赋线的CALayerContent。这样子把这给图片提交 GPU 的时候,GPU 就作为纹理直接显示出来,减轻 GPU 的压力。

CPU 和 GPU 他们之间的关系并不像上图一样是平行关系,实际上最后都交给GPU 进行处理。

正如上图所示,GPU 面临下面挑战:GPU 需要把每一帧 的纹理(位图)合成在一起(60次/秒)。每一个纹理会占用 VRAM(video RAM),所以 GPU 同时持有纹理的数量是有限制的。GPU 在合成方面非常高效,但是某些合成任务却比其他更复杂,并且 GPU在 16.7ms(1/60s)内能做的工作也是有限的。

另外的挑战就是将数据传输到 GPU 上。为了让 GPU 访问数据,需要将数据从 RAM 移动到 VRAM 上。这就是所说的上传数据到 GPU。这看起来貌似微不足道,但是一些大型的纹理却会非常耗时。

最终,CPU 开始运行你的程序。你可能会让 CPU 从 bundle 加载一张 PNG 的图片并且解压它,这所有的事情都在 CPU 上进行。然后当你需要显示解压缩后的图片时,它需要以某种方式上传到 GPU。一些看似很普通的任务,比如显示文本,对 CPU 来说却是一件非常复杂的事情,这会促使 Core Text 和 Core Graphics 框架更紧密的集成来根据文本生成一个位图。一旦准备好,它将会被作为一个纹理上传到 GPU 并准备显示出来。当你滚动或者在屏幕上移动文本时,不管怎么样,同样的纹理能够被复用,CPU 只需简单的告诉 GPU 新的位置就行了,所以 GPU 就可以重用存在的纹理了。CPU 并不需要重新渲染文本,并且位图也不需要重新上传到 GPU。

OpenGL/OpenGL ES

它被是一个API(Application Programming Interface, 应用程序编程接口),包含了一系列可以操作图形、图像的函数。然而,OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范(Specification)。

OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由OpenGL库的开发者自行决定(这里开发者是指编写OpenGL库的人)。因为OpenGL规范并没有规定实现的细节,具体的OpenGL库允许使用不同的实现,只要其功能和结果与规范相匹配(亦即,作为用户不会感受到功能上的差异)。

Core Animation

然后咱们回到 iOS 中,界面渲染流程如下:

屏幕页面流畅的时候帧率应该是60帧/秒,每一帧的时间大概就是16.67ms。
由图片上可见到GPU渲染之前分为三步:

  1. CoreAnimation 提交事务,把图层树提交到Render Server,此过程是发生在你的应用程序内部
  2. Render Server 是一个单独的进程,在 iOS6 以后叫BackBoard,它负责解析图层树,反序列化为渲染树
  3. 调用绘制指令,并提交到 GPU

Commit Transaction

基于上面的内容,我们能控制的只有 Commit Transaction 里面的操作。而 CPU 优化也通常在这一步。

  • Layout(布局)
    • 调用layoutSubview方法
    • 创建ViewaddSubview方法
    • 会造成CPUI/O瓶颈

      减少view的层级,可以layoutSubview方法的运行时间,使用大量的xib之类的,也会造成I/0性能。

  • Display(显示)

    • 调用 drawRect:(如果重写了)
    • 绘制字符串
    • 会造成CPU和内存瓶颈

      drawRect:方法里面绘制大量的文字同样会对CPU造成影响,并且drawRect:方法会创建一个绘制上下文,这个上下文的大小所占的内存不难算出来:图层宽 图层高 4字节,iPhone6s plus的上下文内存就是 1920 1080 5 * 4字节,为79M内存。除非很有必要,否则应该避免重绘视图

  • Prepare(准备)

    • Image decoding 图片解码
    • Image conversion 图片变换

      图片加载与图片解码常常CPU和内存消耗大户。一般来说,iOS设备的闪存加载大量图片还是会影响图片文件的加载速度,就需要异步加载到内存中,并且需要及时回收内存。图片解码过程是一个相当复杂的任务,需要消耗非常长的时间,解码后的图片同样会占用相当大的内存,有些时候就需要强制解码。

  • Commit(提交)
    • 打包Layers并通过IPC发送到渲染服务
    • 递归提交子树的Layer,如果图层树太复杂会消耗很大,对性能有很大的影响

渲染时机

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

摘自:深入理解RunLoop