对于iOS的性能优化,最能体现在用户端的就是界面的流畅,如何保持界面的流畅是我们作为开发要追求的,本章节先来介绍一下界面展示的相关原理。
1.硬件显示原理
屏幕基础渲染原理
首先从过去的 CRT
显示器原理说起。CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization
),简称 HSync
;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization
),简称 VSync
。显示器通常以固定频率进行刷新,这个刷新率就是VSync
信号产生的频率。尽管现在的设备大都是液晶显示屏了,但原理仍然没有变。
- 垂直同步信号(
VSync
):
屏幕发出VSync之后,就表示将要进行新一帧画面的显示,于是开始从帧缓存里面读取经过GPU
渲染好的用于显示的数据 - 水平同步信号(
HSync
):
显示器从帧缓存里拿到数据之后,是从上到下一行一行的刷新的,刷新完一行,就发出一个HSync,直到最下面一层显示出来,这样,一帧的画面就完成了显示。
iOS中的渲染过程
在iOS的界面渲染中,也是需要遵循上述的屏幕渲染原理的,这是一系列复杂过程,主要使用了CPU
,GPU
和对应的双缓存机制
- CPU(Central Processing Unit):
- 中央处理器,在iOS程序中,负责对象的创建和销毁、对象属性的调整、布局的计算、文本的计算和排版规格、图片的格式转码和解码、图像的绘制(Core Graphic)
- GPU(Graphics Processing Unit):
- 图形处理器,负责纹理的渲染。如果没有接触过OpenGL的朋友,可能不太好理解纹理渲染这个概念,我们知道,屏幕上面的物理元件是像素,我们在屏幕上面看到的图片,文字,视频,就是由屏幕上的所有像素,通过控制色值变化而呈现出来的。那么像素的色值数据,就是由GPU计算得出的,然后将这些数据提交给视频控制器,由它负责显示到屏幕上。
- 比CPU使用更少的电来完成工作并且GPU的浮点计算能力要超出CPU很多。
- GPU的渲染性能要比CPU高效很多,同时对系统的负载和消耗也更低一些,所以在开发中,我们应该尽量让CPU负责主线程的UI调动,把图形显示相关的工作交给GPU来处理,当涉及到光栅化等一些工作时,CPU也会参与进来
- 双缓冲机制:
- iOS中采用的是双缓冲机制,分为前帧缓存和后帧缓存。
- GPU会预先渲染好一帧放入一个缓冲区内(前帧缓存),让视频控制器读取,当下一帧渲染好后,GPU会直接把视频控制器的指针指向第二个缓冲器(后帧缓存)
- 当你视频控制器已经读完一帧,准备读下一帧的时候,GPU会等待显示器的VSync信号发出后,前帧缓存和后帧缓存会瞬间切换,后帧缓存会变成新的前帧缓存,同时旧的前帧缓存会变成新的后帧缓存。
2.卡顿原因
我们手机屏幕的刷帧率是60FPS
(Frame per Second 帧/秒),也就是会所1秒钟的时间,屏幕可以刷新60帧(次)。完成一帧刷新的用时是16.6毫秒。因此垂直同步信号VSync就是每16.6
毫秒发出一次。
通过对上面界面渲染原理的探究,可以总结出造成卡顿的主要原因就是:
- 在一个 VSync 时间段内,CPU 或者 GPU 没有完成内容提交,阻碍了显示流程,造成掉了帧现象
-
Vsync 与双缓冲的意义:强制同步屏幕刷新,以掉帧为代价解决屏幕撕裂问题。
3.iOS 中的渲染框架
通过上面基础原理的探究,对屏幕的渲染有了一定的了解。那么在iOS中的整体渲染流程,基本如上图所示。在硬件基础之上,iOS 中有
Core Graphics
、Core Animation
、Core Image
、OpenGL
、Metal
等多种软件框架来绘制内容,在 CPU 与 GPU 之间进行了更高层地封装。
-
GPU Driver
:上述软件框架相互之间也有着依赖关系,不过所有框架最终都会通过 OpenGL 连接到 GPU Driver,GPU Driver 是直接和 GPU 交流的代码块,直接与 GPU 连接。 -
OpenGL
:是一个提供了 2D 和 3D 图形渲染的 API,它能和 GPU 密切的配合,最高效地利用 GPU 的能力,实现硬件加速渲染。OpenGL的高效实现(利用了图形加速硬件)一般由显示设备厂商提供,而且非常依赖于该厂商提供的硬件。OpenGL 之上扩展出很多东西,如 Core Graphics 等最终都依赖于 OpenGL,有些情况下为了更高的效率,比如游戏程序,甚至会直接调用 OpenGL 的接口。 -
Metal
:Metal 类似于 OpenGL ES,也是一套第三方标准,具体实现由苹果实现。Core Animation、Core Image、SceneKit、SpriteKit 等等渲染框架都是构建于 Metal 之上的。 -
Core Graphics
:Core Graphics 是一个强大的二维图像绘制引擎,是 iOS 的核心图形库,常用的比如 CGRect 就定义在这个框架下。 -
Core Image
:Core Image 是一个高性能的图像处理分析的框架,它拥有一系列现成的图像滤镜,能对已存在的图像进行高效的处理。 -
Core Animation
:在 iOS 上,几乎所有的东西都是通过 Core Animation 绘制出来,它的自由度更高,使用范围也更广。
4.CoreAnimation渲染原理
CoreAnimation初探
Core Animation
,它本质上可以理解为一个复合引擎,主要职责包含:渲染、构建和实现动画
Core Animation
是 AppKit 和 UIKit 完美的底层支持,同时也被整合进入 Cocoa 和 Cocoa Touch 的工作流之中,它是 app 界面渲染和构建的最基础架构。 Core Animation 的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的 layer(iOS 中具体而言就是 CALayer
),并且被存储为树状层级结构。这个树也形成了 UIKit 以及在 iOS 应用程序当中你所能在屏幕上看见的一切的基础。
UIView和CALayer关系
在CoreAnimation的渲染中,与开发者关系最大的就是UIView
和CALayer
了,为了能更好的理解CoreAnimation的渲染流程,我们必须明确这两者之间的关系。
UIView
UIView - Apple
Views are the fundamental building blocks of your app's user interface, and theUIView
class defines the behaviors that are common to all views. A view object renders content within its bounds rectangle and handles any interactions with that content.
根据 Apple 的官方文档,UIView 是 app 中的基本组成结构,定义了一些统一的规范。它会负责内容的渲染以及,处理交互事件。具体而言,它负责的事情可以归为下面三类
-
Drawing and animation
:绘制与动画 -
Layout and subview management
:布局与子 view 的管理 -
Event handling
:点击事件处理
CALayer
CALayer - Apple
Layers are often used to provide the backing store for views but can also be used without a view to display content. A layer’s main job is to manage the visual content that you provide...
If the layer object was created by a view, the view typically assigns itself as the layer’s delegate automatically, and you should not change that relationship.
CALayer 的官方文档中我们可以看出,CALayer 的主要职责是管理内部的可视内容。当我们创建一个 UIView 的时候,UIView 会自动创建一个 CALayer,为自身提供存储 bitmap
的地方,并将自身固定设置为 CALayer 的代理
。
那么CALayer
是如何展示bitmap
视图的呢?
/** Layer content properties and methods. **/
/* An object providing the contents of the layer, typically a CGImageRef,
* but may be something else. (For example, NSImage objects are
* supported on Mac OS X 10.6 and later.) Default value is nil.
* Animatable. */
@property(nullable, strong) id contents;
在CALayer的源码中,我们发现了contents 提供了 layer 的内容,是一个指针类型,在 iOS 中的类型就是 CGImageRef
(在 OS X 中还可以是 NSImage)。而我们进一步查到,Apple 对 CGImageRef
的定义是:
A bitmap image or image mask.
看到 bitmap,这下我们就可以和之前讲的的渲染流水线联系起来了:实际上,CALayer 中的 contents 属性保存了由设备渲染流水线渲染好的位图 bitmap(通常也被称为 backing store
),而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的 bitmap,进而呈现到屏幕上。
正因为每次要被渲染的内容是被静态的存储起来的,所以每次渲染时,Core Animation 会触发调用 drawRect: 方法,使用存储好的 bitmap 进行新一轮的展示。
// 注意 CGImage 和 CGImageRef 的关系:
// typedef struct CGImage CGImageRef;
layer.contents = (__bridge id)image.CGImage;
两者关系
- 每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView 的尺寸样式都由内部的 Layer 所提供。两者都有树状层级结构,layer 内部有 SubLayers,View 内部有 SubViews.但是 Layer 比 View 多了个AnchorPoint
- 在 View显示的时候,UIView 做为 Layer 的 CALayerDelegate,View 的显示内容由内部的 CALayer 的 display
- CALayer 是默认修改属性支持隐式动画的,在给 UIView 的 Layer 做动画的时候,View 作为 Layer 的代理,Layer 通过 actionForLayer:forKey:向 View请求相应的 action(动画行为)
- layer 内部维护着三分 layer tree,分别是 presentLayer Tree(动画树),modeLayer Tree(模型树), Render Tree (渲染树),在做 iOS动画的时候,我们修改动画的属性,在动画的其实是 Layer 的 presentLayer的属性值,而最终展示在界面上的其实是提供 View的modelLayer
两者主要的关系如下:
- CALayer 是 UIView 的属性之一,负责渲染和动画,提供可视内容的呈现。
- UIView 提供了对 CALayer 部分功能的封装,同时也另外负责了交互事件的处理。
两者主要的异同点如下:
相同的层级结构:我们对 UIView 的层级结构非常熟悉,由于每个 UIView 都对应 CALayer 负责页面的绘制,所以 CALayer 也具有相应的层级结构。
部分效果的设置:因为 UIView 只对 CALayer 的部分功能进行了封装,而另一部分如圆角、阴影、边框等特效都需要通过调用 layer 属性来设置。
是否响应点击事件:CALayer 不负责点击事件,所以不响应点击事件,而 UIView 会响应。
不同继承关系:CALayer 继承自
NSObject
,UIView 由于要负责交互事件,所以继承自UIResponder
。
当然还剩最后一个问题,为什么要将 CALayer 独立出来,直接使用 UIView 统一管理不行吗?为什么不用一个统一的对象来处理所有事情呢?
这样设计的主要原因就是为了职责分离,拆分功能,方便代码的复用
。
通过 Core Animation 框架来负责可视内容的呈现,这样在 iOS 和 OS X 上都可以使用 Core Animation 进行渲染。与此同时,两个系统还可以根据交互规则的不同来进一步封装统一的控件,比如 iOS 有 UIKit 和 UIView,OS X 则是AppKit 和 NSView。
CoreAnimation渲染流程
关于CoreAnimation的渲染流程,通过上图可以比较清晰的看出,主要可以总结为以下步骤:
-
Handle Events
:这个过程中会先处理点击事件,这个过程中有可能会需要改变页面的布局和界面层次。 -
Commit Transaction
:此时 app 会通过 CPU 处理显示内容的前置计算,比如布局计算、图片解码等任务。之后将计算好的图层进行打包发给 Render Server。 -
Decode
:打包好的图层被传输到 Render Server 之后,首先会进行解码。注意完成解码之后需要等待下一个RunLoop
才会执行下一步 Draw Calls。 -
Draw Calls
:解码完成后,Core Animation 会调用下层渲染框架(比如 OpenGL 或者 Metal)的方法进行绘制,进而调用到 GPU。 -
Render
:这一阶段主要由 GPU 进行渲染。 -
Display
:显示阶段,需要等 render 结束的下一个 RunLoop 触发显示。
Commit Transaction 渲染原理
在日常的开发中,作为开发者能影响到的就是 Handle Events 和 Commit Transaction 这两个阶段,这也是开发者接触最多的部分。Handle Events 就是处理触摸事件,而 Commit Transaction 这部分中主要进行的是:Layout
、Display
、Prepare
、Commit
等四个具体的操作。
Layout
这个阶段主要是构建视图,遍历的操作[UIView layerSubview]
,[CALayer layoutSubLayers]
- 调用重载的
layoutSubviews
方法 - 创建视图,并通过
addSubview
方法添加子视图 - 计算视图布局,即所有的
Layout Constraint
由于这个阶段是在 CPU
中进行,通常是 CPU 限制或者 IO 限制,所以我们应该尽量高效轻量地操作,减少这部分的时间,比如减少非必要的视图创建、简化布局计算、减少视图层级等。代码的主要调用结构如下:
Display
这个阶段主要是交给 Core Graphics 进行视图的绘制,注意不是真正的显示,而是得到前文所说的contents数据。
根据UIView和CALayer的关系,我们知道,主要是CALayer来负责一个view的展示,并最终将得到的bitmap
赋值给contents
属性,保存在backing store
中供后续使用。
我们知道view的绘制会在drawRect:
方法中,我们在其中打断点,可得到一下堆栈信息
通过调用堆栈,可以得到此过程主要如下:
- 根据Layout获得的数据,进行展示
- 通过CALayer和UIView之间的代理来进行展示,主要实现在的
display
方法中
- CALayer中实现
[self drawInContext:context]
: 传入上下文进行绘制 - UIView中实现
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
:对界面进行绘制并传入上下文 - UIView中实现
- (void)displayLayer:(CALayer *)layer
:通过UIGraphicsGetImageFromCurrentImageContext
获取到当前上下文的图片,并赋值给layer.contents = (__bridge id)(image.CGImage);
- 最后使用
UIGraphicsEndImageContext
关闭上下文
- 以上是默认的流程,如果自己重写实现了
drawRect:
,这个方法会直接调用Core Graphics
绘制方法得到bitmap
数据,同时系统会额外申请一块内存
,用于暂存绘制好的bitmap
。这样绘制过程从GPU
转移到了CPU
,这就导致了一定的效率损失。与此同时,这个过程会额外使用 CPU 和内存,因此需要高效绘制,否则容易造成 CPU 卡顿或者内存爆炸。
Prepare
Core Animation 额外的工作,主要图片解码和转换
Commit
打包图层并将它们发送到 Render Server
。
注意 commit 操作是依赖图层树递归执行的,所以如果图层树过于复杂,commit 的开销就会很大。这也是我们希望减少视图层级,从而降低图层树复杂度的原因。
Render Server相关
Render Server 通常是 OpenGL
或者是 Metal
。以 OpenGL 为例,那么上图主要是 GPU 中执行的操作,具体主要包括:
- GPU 收到
Command Buffer
,包含图元 primitives 信息 -
Tiler
开始工作:先通过顶点着色器Vertex Shader
对顶点进行处理,更新图元信息 - 平铺过程:平铺生成
tile bucket
的几何图形,这一步会将图元信息转化为像素,之后将结果写入Parameter Buffer
中 -
Tiler
更新完所有的图元信息,或者 Parameter Buffer 已满,则会开始下一步 -
Renderer
工作:将像素信息进行处理得到bitmap
,之后存入Render Buffer
-
Render Buffer
中存储有渲染好的bitmap
,供之后的Display
操作使用
使用 Instrument 的 OpenGL ES,可以对过程进行监控。OpenGL ES tiler utilization 和 OpenGL ES renderer utilization 可以分别监控 Tiler 和 Renderer 的工作情况
参考
iOS性能优化
iOS保持界面流畅
iOS Rendering 渲染全解析
深入理解 iOS Rendering Process