八、视图的显示原理

Reference:
绘制像素到屏幕上
iOS 保持界面流畅的技巧
iOS界面渲染流程分析
iOS开发-视图渲染与性能优化

显示逻辑

iOS的APP运行起来后,主线程的RunLoop可以被用户交互、定时任务、GPU通知等所唤醒,唤醒后处理事件,完成对视图的更改;在RunLoop再次休眠前,根据所做的更改,由CPU完成视图约束更新、布局和绘制;再通过OpenGL ES(或Metal)生成纹理,提交给GPU;GPU对纹理进行合成渲染,将渲染结果提交到帧缓存区;最后屏幕根据自己的刷新率,定期获取帧缓存区中的数据进行显示。

垂直同步

屏幕都有一个固定的刷新率,屏幕会按照这个频率去将GPU帧缓存区(FrameBuffer)中的内容重新显示,目前iOS设备屏幕的刷新率都是60Hz(59.97Hz)

在一帧画面的显示过程中,如果帧缓存区的数据发生了替换,屏幕就会显示“撕裂”画面(一部分上一帧画面,一部分下一帧画面),GPU通过垂直同步机制(V-Sync)来解决这个问题:屏幕在完成一帧画面的显示后会发出一个VSync(垂直同步信号),VSync的产生频率和屏幕的刷新率一致,GPU收到VSync后再替换帧缓存区中的数据。

GPU渲染一帧数据需要时间,如果在收到VSync之后才开始渲染需要立即显示的画面,显然就会有延时,所以便需要双缓冲机制:屏幕显示FrameBuffer中的内容时,GPU便开始向BackBuffer中准备下一帧的数据,当屏幕显示完成,GPU收到VSync后,下一帧已经渲染完成,GPU直接修改指针,完成FrameBuffer和BackBuffer的替换,并继续为BackBuffer准备新一帧的数据。

iOS设备都使用双缓存,并开启垂直同步,iOS的GPU在收到VSync后,会通过IPC通信唤醒APP主线程的RunLoop,在APP中CPU完成计算,然后在主线程RunLoop再次休眠前,将显示内容提交给GPU渲染,GPU最后再将渲染结果提交到帧缓存区供屏幕显示。

CPU处理

参与模块

APP运行期间,CPU负责管理视图层,并在显示时,为其生成GPU所需的纹理。CPU处理过程中,参与模块之间的架构如图:

在iOS中,OpenGL ES将逐步被新的渲染引擎Metal所替换,Metal针对iOS设备做了高度优化,详情可参见Metal

CALayer与UIView

在CoreAnimation中,一个CALayer表示一个视图,管理着显示相关的信息,CALayer上的属性并不是用实例变量存储的,而是都保存在一个内部字典中;UIView是UIKit对CALayer的装饰,在显示之外主要是添加了支持用户交互相关的功能,UIView背后都有一个CALayer,UIView显示相关的属性和方法(比如 frame/bounds/transform等),都是对背后CALayer属性和方法的映射,通常通过UIView上更加便利的高级API来操作视图,在实现特殊显示需求时,则需要跨过UIView去操作其背后的CALayer。

处理流程

在每一轮视图更新周期中,iOS系统内CPU处理的工作可分为以下五步:

  1. Handle Events: 处理各种事件,完成视图的更改,记录需要更新的视图;
  2. Layout: 根据所做的变更,完成视图层的约束更新与布局;
  3. Display: 完成视图绘制(drawRect)和文本的绘制;
  4. Prepare: 解码图片,将压缩的图片格式转为位图;
  5. Commit: 根据视图层和内容内容生成纹理,提交给GPU合成渲染;

APP通过主线程的RunLoop来Handle Events(四、事件处理机制(RunLoop)),在更改视图的布局和显示内容时,系统不会立即更新视图,而是在RunLoop再次休眠前,集中对需要更新的视图更新约束和布局(七、UIView的布局与刷新),再将视图的显示内容处理成位图,最终与根据视图生成的纹理一起,打包提交给GPU合成渲染。

内容处理

在生成纹理时,视图的布局、颜色等基础信息可直接转换到纹理中,显示内容则需要由CPU处理成位图,再转换为CALayer的contents,最后生成位图纹理提交给GPU,需要处理的内容有三类:图形绘制、图像解码和文本绘制,同样的纹理在GPU中可以被复用,CPU只需要告知新的位置即可,所以如果显示内容没有变更,那么它们通常只在第一次显示时需要处理

图形绘制

iOS中图形绘制通过CoreGraphics或者UIKit中的绘图API来完成,它们的底层都是基于Quartz 2D的,图形绘制时,内容需要写入到一个上下文中(CGContext),可以通过CGBitmapContextCreate创建位图上下文,UIKit中维护着一个上下文栈,使用UIKit的绘图API时(如UIBezierPath等),绘制内容会被写入最顶层的上下文,可以通过UIGraphicsGetCurrentContext来获取最顶层的上下文,还提供了UIGraphicsPushContextUIGraphicsPopContext来控制这个上下文栈,UIGraphicsBeginImageContextWithOptions函数则会创建一个位图上下文,并将其压入UIKit的上下文栈顶,需要注意的是,CoreGraphics创建的上下文是以左下角为原点,UIKit自动创建的上下文则以左上角为原点

自定义UIView的子类如果实现了-drawRect:方法,在绘制时,CoreAnimation会为视图的CALayer申请一个后备存储(CABackingStore),再为后备存储创建绘图上下文,UIKit会将其压入自己的上下文栈顶,-drawRect:中便是对UIKit的栈顶上下文进行绘制,完成绘制后,上下文中的数据会被保存到后备存储中,后备存储则被设置为视图CALayer的contents

图像解码

图像通常以JPEG或PNG的压缩格式保存在文件中,在根据图像文件生成UIImage,并赋给UIImageView显示时,系统完成了图像的加载,但要在第一次显示的纹理提交前(Prepare阶段)才对图像进行解码,得到位图数据,生成一个CGImage,作为UIImageView的CALayer的contents,系统对图像的加载和解码都在主线程中进行。

JPEG是一种有损压缩,PNG是无损压缩,JPEG的解码比PNG复杂得多,在APP编译打包时,Xcode会对PNG进行优化,优化后的PNG可以被iOS读取,并且解压的速度更快。

文本绘制

文本在绘制前需要排版(TextLayout),UIKit中文本显示视图(UILabel和UITextView)的文本排版和绘制都在主线程中完成,其中TextLayout的计算开销较大,详情推荐阅读深入理解Autolayout与列表性能,绘制则是由CoreText和CoreGraphics合作生成一张位图,与图形绘制相似,绘制好的位图数据被存放在一个后备存储(CABackingStore)中,作为文本显示视图的CALayer的contents

GPU渲染

Tile-Based渲染

移动端GPU都使用了Tile-Based渲染技术,Full-Screen和Tile-Based在渲染时都会将屏幕分成多个Tile进行处理,但移动端中GPU对渲染缓存的访问开销更大,Tile-Based将顶点处理和像素处理分开进行,在一帧的顶点处理完成后,再一次性访问渲染缓存,按Tile顺序进行像素处理,减少了渲染时GPU对帧缓存区的操作次数,提升了性能的同时也减少了耗电。

Tile-Based的渲染流程:

  1. Command Buffer: 接收渲染引擎(OpenGL ES或Metal)传递的渲染指令;
  2. Tiler: 调用顶点着色器,进行顶点处理,将顶点数据分块(Tiling);
  3. Parameter Buffer: 接收顶点处理的结果和相应的渲染参数;
  4. Renderer: 调用像素着色器(又叫片元着色器),进行像素处理;
  5. Render Buffer: 缓存渲染完毕的数据;

视图渲染需要将纹理合成显示,屏幕上每一个像素的颜色都需要进行混合计算,当源纹理是完全不透明的,目标像素的颜色就等于源纹理上的颜色,使用不透明的图层和没有alpha通道的位图,可以有效的减轻GPU渲染合成时的运算压力。

渲染等待

在Tile-Based的渲染中,每一帧的顶点处理和像素处理相对独立,再加上CPU处理,iOS会将它们安排在相邻的三帧中,一帧的渲染命令提交后,会在之后的第三帧显示。

离屏渲染

离屏渲染(Offscreen Rendering)是将内容渲染到一个屏幕渲染缓存之外的缓存区,离屏渲染在普通渲染之后进行,再与前面的渲染结果合并(Compositing),离屏渲染的结果可以被复用,离屏缓存区的大小大概是屏幕缓存区的两倍。离屏渲染可以避免内容的重复渲染,但会触发多次的环境切换,开销本身就不小,开启离屏渲染是否会提示整体的渲染速度,需要根据具体的情况进行权衡。

离屏渲染可以手动强制开启,也可以在一些视图设置后,由CoreAnimation自动开启。通过设置shouldRasterize将CALayer光栅化,可以强制对视图离屏渲染,光栅化是指将几何数据转换为像素数据,开启光栅化的CALayer会作为位图,并由离屏渲染进行处理。CALayer的Shadow(阴影)和Mask(遮罩)将自动触发离屏渲染,Mask实际上是一个拥有alpha值的位图,每个CALayer都可以有一个关联的Mask,有的视图的圆角便是通过Mask实现的,更多设置视图自动触发离屏渲染的实例可以参考文章iOS 阴影,圆角,避免离屏渲染

画面丢帧

由于垂直同步机制,CPU或者GPU如果未在两个VSync之间(16.67ms)完成内容的提交,那一帧就会被丢弃,屏幕继续显示之前的内容,画面显示发生丢帧。FPS是屏幕每秒显示的帧数,iOS在显示动画时,理想值是60FPS,为了不让用户感觉到明显的卡顿,FPS应保证不低于30,可以借助CADiplayLink对FPS进行监控(参考YYText/YYFpsLabel),当因为丢帧产生画面卡顿时,基于iOS视图的显示原理,可行的优化思路如下:

  1. Handle Events: 不要在主线程中执行耗时过长的代码,尽量避免在一个周期中多次调整视图的frame/bounds/center等属性;
  2. Layout: 减少视图的层级,使用清晰的约束,缓存布局和TextLayout的结果等;
  3. Display: 自定义视图实现文本和绘图的异步绘制;
  4. Prepare: 用解码效率更高的PNG替换JPEG,提前异步完成图片的解码;
  5. Commit: 避免引入不必要的视图和视图层级;
  6. 普通渲染: 尽量使用不透明图层和不含alpha通道的图片,以减轻渲染时的合成计算;
  7. 离屏渲染: 动态列表中应避免触发离屏渲染,静态内容是否使用离屏渲染需要权衡;

在优化视图显示速度,提升动画流畅度方面,iOS中一些成熟的第三方方案,可以作为直接的工具或重要的参考:

  • Panda: 官方Autolayout的替代方案,优化了对Cassowary算法的使用,对布局和TextLayout进行缓存,支持异步绘制等,可以优化视图的约束跟新与布局阶段;
  • YYText: 自定义的文本显示相关视图,异步绘制文本,并缓存文本排版和绘制的结果,可以优化视图的文本绘制阶段;
  • AsyncDisplayKit: fackbook开源的异步显示框架,充分利用CPU的并行计算能力,延迟创建和更新视图,异步进行布局和绘制的计算,提前对部分图层合并绘制等,对视图显示各阶段均有优化,框架比较重量级,引入Flexbox布局,有一定的门槛,本身也建议仅在确实需要优化的页面使用。
-------------This article is over, thank you for reading -------------