iOS性能优化 - 界面显示原理

对于iOS的性能优化,最能体现在用户端的就是界面的流畅,如何保持界面的流畅是我们作为开发要追求的,本章节先来介绍一下界面展示的相关原理。

1.硬件显示原理

屏幕基础渲染原理

首先从过去的 CRT 显示器原理说起。CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是VSync信号产生的频率。尽管现在的设备大都是液晶显示屏了,但原理仍然没有变。

  • 垂直同步信号(VSync):
    屏幕发出VSync之后,就表示将要进行新一帧画面的显示,于是开始从帧缓存里面读取经过GPU渲染好的用于显示的数据
  • 水平同步信号(HSync):
    显示器从帧缓存里拿到数据之后,是从上到下一行一行的刷新的,刷新完一行,就发出一个HSync,直到最下面一层显示出来,这样,一帧的画面就完成了显示。
iOS性能优化 - 界面显示原理_第1张图片
屏幕渲染原理

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信号发出后,前帧缓存和后帧缓存会瞬间切换,后帧缓存会变成新的前帧缓存,同时旧的前帧缓存会变成新的后帧缓存。
iOS性能优化 - 界面显示原理_第2张图片
iOS屏幕渲染机制

2.卡顿原因

我们手机屏幕的刷帧率是60FPS(Frame per Second 帧/秒),也就是会所1秒钟的时间,屏幕可以刷新60帧(次)。完成一帧刷新的用时是16.6毫秒。因此垂直同步信号VSync就是每16.6毫秒发出一次。

通过对上面界面渲染原理的探究,可以总结出造成卡顿的主要原因就是:

  • 在一个 VSync 时间段内,CPU 或者 GPU 没有完成内容提交,阻碍了显示流程,造成掉了帧现象
  • Vsync 与双缓冲的意义:强制同步屏幕刷新,以掉帧为代价解决屏幕撕裂问题。


    iOS性能优化 - 界面显示原理_第3张图片
    卡顿原因展示

3.iOS 中的渲染框架

iOS性能优化 - 界面显示原理_第4张图片
iOS渲染框架1

通过上面基础原理的探究,对屏幕的渲染有了一定的了解。那么在iOS中的整体渲染流程,基本如上图所示。在硬件基础之上,iOS 中有 Core GraphicsCore AnimationCore ImageOpenGLMetal 等多种软件框架来绘制内容,在 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初探

CoreAnimation

Core Animation,它本质上可以理解为一个复合引擎,主要职责包含:渲染、构建和实现动画

Core Animation 是 AppKit 和 UIKit 完美的底层支持,同时也被整合进入 Cocoa 和 Cocoa Touch 的工作流之中,它是 app 界面渲染和构建的最基础架构。 Core Animation 的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的 layer(iOS 中具体而言就是 CALayer),并且被存储为树状层级结构。这个树也形成了 UIKit 以及在 iOS 应用程序当中你所能在屏幕上看见的一切的基础。

UIView和CALayer关系

在CoreAnimation的渲染中,与开发者关系最大的就是UIViewCALayer了,为了能更好的理解CoreAnimation的渲染流程,我们必须明确这两者之间的关系。

UIView

UIView - Apple
Views are the fundamental building blocks of your app's user interface, and the UIView 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 内部都有一个 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

两者主要的关系如下:

  1. CALayer 是 UIView 的属性之一,负责渲染和动画,提供可视内容的呈现。
  2. 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渲染流程

iOS性能优化 - 界面显示原理_第5张图片
CoreAnimation渲染流程

关于CoreAnimation的渲染流程,通过上图可以比较清晰的看出,主要可以总结为以下步骤:

  1. Handle Events:这个过程中会先处理点击事件,这个过程中有可能会需要改变页面的布局和界面层次。
  2. Commit Transaction:此时 app 会通过 CPU 处理显示内容的前置计算,比如布局计算、图片解码等任务。之后将计算好的图层进行打包发给 Render Server。
  3. Decode:打包好的图层被传输到 Render Server 之后,首先会进行解码。注意完成解码之后需要等待下一个 RunLoop才会执行下一步 Draw Calls。
  4. Draw Calls:解码完成后,Core Animation 会调用下层渲染框架(比如 OpenGL 或者 Metal)的方法进行绘制,进而调用到 GPU。
  5. Render:这一阶段主要由 GPU 进行渲染。
  6. Display:显示阶段,需要等 render 结束的下一个 RunLoop 触发显示。

Commit Transaction 渲染原理

在日常的开发中,作为开发者能影响到的就是 Handle Events 和 Commit Transaction 这两个阶段,这也是开发者接触最多的部分。Handle Events 就是处理触摸事件,而 Commit Transaction 这部分中主要进行的是:LayoutDisplayPrepareCommit等四个具体的操作。

Layout

这个阶段主要是构建视图,遍历的操作[UIView layerSubview][CALayer layoutSubLayers]

  • 调用重载的 layoutSubviews 方法
  • 创建视图,并通过 addSubview方法添加子视图
  • 计算视图布局,即所有的Layout Constraint

由于这个阶段是在 CPU 中进行,通常是 CPU 限制或者 IO 限制,所以我们应该尽量高效轻量地操作,减少这部分的时间,比如减少非必要的视图创建、简化布局计算、减少视图层级等。代码的主要调用结构如下:

iOS性能优化 - 界面显示原理_第6张图片
Layout调用伪代码

Display

这个阶段主要是交给 Core Graphics 进行视图的绘制,注意不是真正的显示,而是得到前文所说的contents数据。

根据UIView和CALayer的关系,我们知道,主要是CALayer来负责一个view的展示,并最终将得到的bitmap赋值给contents属性,保存在backing store中供后续使用。

我们知道view的绘制会在drawRect:方法中,我们在其中打断点,可得到一下堆栈信息

iOS性能优化 - 界面显示原理_第7张图片
drawRect:绘制堆栈

通过调用堆栈,可以得到此过程主要如下:

  1. 根据Layout获得的数据,进行展示
  2. 通过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关闭上下文
  1. 以上是默认的流程,如果自己重写实现了drawRect:,这个方法会直接调用 Core Graphics绘制方法得到bitmap数据,同时系统会额外申请一块内存,用于暂存绘制好的bitmap。这样绘制过程从 GPU 转移到了CPU,这就导致了一定的效率损失。与此同时,这个过程会额外使用 CPU 和内存,因此需要高效绘制,否则容易造成 CPU 卡顿或者内存爆炸
iOS性能优化 - 界面显示原理_第8张图片
Display伪代码

Prepare

Core Animation 额外的工作,主要图片解码和转换

Commit

打包图层并将它们发送到 Render Server

注意 commit 操作是依赖图层树递归执行的,所以如果图层树过于复杂,commit 的开销就会很大。这也是我们希望减少视图层级,从而降低图层树复杂度的原因。

Render Server相关

Render Server 通常是 OpenGL或者是 Metal。以 OpenGL 为例,那么上图主要是 GPU 中执行的操作,具体主要包括:

  1. GPU 收到Command Buffer,包含图元 primitives 信息
  2. Tiler 开始工作:先通过顶点着色器 Vertex Shader对顶点进行处理,更新图元信息
  3. 平铺过程:平铺生成 tile bucket的几何图形,这一步会将图元信息转化为像素,之后将结果写入Parameter Buffer
  4. Tiler更新完所有的图元信息,或者 Parameter Buffer 已满,则会开始下一步
  5. Renderer工作:将像素信息进行处理得到bitmap,之后存入 Render Buffer
  6. Render Buffer 中存储有渲染好的 bitmap,供之后的 Display 操作使用

使用 Instrument 的 OpenGL ES,可以对过程进行监控。OpenGL ES tiler utilization 和 OpenGL ES renderer utilization 可以分别监控 Tiler 和 Renderer 的工作情况

参考

iOS性能优化
iOS保持界面流畅
iOS Rendering 渲染全解析
深入理解 iOS Rendering Process

你可能感兴趣的:(iOS性能优化 - 界面显示原理)