面试题优化

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

2020-06-12  本文已影响0人  低调的默认名

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

1.硬件显示原理

屏幕基础渲染原理

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

屏幕渲染原理

iOS中的渲染过程

在iOS的界面渲染中,也是需要遵循上述的屏幕渲染原理的,这是一系列复杂过程,主要使用了CPU,GPU和对应的双缓存机制

iOS屏幕渲染机制

2.卡顿原因

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

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

3.iOS 中的渲染框架

iOS渲染框架1
通过上面基础原理的探究,对屏幕的渲染有了一定的了解。那么在iOS中的整体渲染流程,基本如上图所示。在硬件基础之上,iOS 中有 Core GraphicsCore AnimationCore ImageOpenGLMetal 等多种软件框架来绘制内容,在 CPU 与 GPU 之间进行了更高层地封装。

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 中的基本组成结构,定义了一些统一的规范。它会负责内容的渲染以及,处理交互事件。具体而言,它负责的事情可以归为下面三类

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

两者主要的关系如下:

  1. CALayer 是 UIView 的属性之一,负责渲染和动画,提供可视内容的呈现。
  2. UIView 提供了对 CALayer 部分功能的封装,同时也另外负责了交互事件的处理。

两者主要的异同点如下:

当然还剩最后一个问题,为什么要将 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 这部分中主要进行的是:LayoutDisplayPrepareCommit等四个具体的操作。

Layout

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

由于这个阶段是在 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方法中
  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

上一篇 下一篇

猜你喜欢

热点阅读