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

对于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信号发出后,前帧缓存和后帧缓存会瞬间切换,后帧缓存会变成新的前帧缓存,同时旧的前帧缓存会变成新的后帧缓存。
iOS屏幕渲染机制

2.卡顿原因

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

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

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


    卡顿原因展示

3.iOS 中的渲染框架

iOS渲染框架1

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

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 这部分中主要进行的是:Layout、Display、Prepare、Commit等四个具体的操作。

Layout

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

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

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

Layout调用伪代码

Display

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

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

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

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 卡顿或者内存爆炸
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

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352