iOS 渲染小结

2020-07-14  本文已影响0人  z4ywzrq

原文链接:http://www.yupeng.fun/2020/07/02/render/

本文整理一下有关计算机图像渲染流程,以及 iOS 渲染相关知识,最后介绍一下在 iOS 开发过程中保持 APP 流畅的注意事项。

简介

在显示器上显示的图像是由一帧一帧的画面组成的,当一帧画面绘制完成后,准备画下一帧,显示器会发出一个垂直同步信号 VSync(vertical synchronization)刷新画面。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。
计算机通过 CPU、GPU、显示器协同工作显示图像。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

计算机将存储在内存中的形状转换成实际绘制在屏幕上的图像的过程称为渲染( Render )。下面就来看一下渲染的过程。

计算机图像渲染

图像渲染流程,大概的步骤:

Application 应用处理阶段:得到图元

这个阶段图像在应用中被处理,可能会对图像进行一系列的操作或者改变,此时还处于 CPU 负责的时期。最终将新的图像信息传给下一阶段。这部分信息被叫做图元(primitives)用于表示渲染的顶点数据,如:点、线、三角形。

Geometry 几何处理阶段:处理图元

进入这个阶段之后,就主要由 GPU 负责了。GPU 拿到上一个阶段传递下来的图元信息,对这部分图元进行处理,之后输出新的图元。这一系列阶段包括:

Rasterization 光栅化阶段:图元转换为像素

光栅化的主要目的是将几何渲染之后的图元信息数据,转换为一系列的像素,以便后续显示在屏幕上。根据图元信息,计算出每个图元所覆盖的像素信息,生成片段。片段(Fragment) 是渲染一个像素所需要的所有数据。

Pixel 像素处理阶段:处理像素,得到位图

经过上述光栅化阶段,我们得到了图元所对应的像素,此时,我们需要给这些像素填充颜色和效果,只要有足够多的不同色彩的像素,就可以制作出色彩丰富的图象。所以最后这个阶段就是给像素填充正确的内容,最终显示在屏幕上。这些经过处理、蕴含大量信息的像素点集合,被称作位图(bitmap)。也就是说,Pixel 阶段最终输出的结果就是位图,过程具体包含:

图像渲染流程结束之后,接下来就需要将得到的像素信息显示在物理屏幕上了。GPU 最后一步渲染结束之后像素信息,被存在帧缓冲器(Framebuffer)中,之后视频控制器(Video Controller)会读取帧缓冲器中的信息,经过数模转换传递给显示器进行显示。

iOS 中的渲染

iOS 的渲染框架依然符合渲染流水线的基本架构。在硬件基础之上,iOS 中有 Core Graphics、Core Animation、Core Image、OpenGL 等多种软件框架来绘制内容,在 CPU 与 GPU 之间进行了更高层地封装。

UIKit 是 iOS 开发者最常用的框架,可以通过设置 UIKit 组件的布局以及相关属性来绘制界面。显示、动画都通过 CoreAnimation,依赖于 OpenGL ES、Metal 做 GPU 渲染,CoreGraphics 做 CPU 渲染,最底层的 GraphicsHardWare 是图形硬件。

显示在屏幕上的 UIView 继承自 UIResponder 自身并不具备在屏幕成像的能力,其主要负责对用户操作事件的响应。我们看到的屏幕上的内容都由 CALayer 进行管理,CALayer 中有个属性 contents 提供了 layer 的内容。contents 属性保存了由设备渲染流水线渲染好的位图 bitmap(通常也被称为 backing store),而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的 bitmap,进而呈现到屏幕上。每次渲染 需要重绘时,Core Animation 会触发调用 drawRect: 方法,使用存储好的 bitmap 进行新一轮的展示。

渲染过程

iOS 中 APP 的渲染是由一个独立的进程 Render Server 负责。APP 将渲染任务及相关数据提交给 Render Server。Render Server 处理完数据后,再传递至 GPU。最后由 GPU 调用 iOS 的图像设备进行显示。
1、CoreAnimation 提交会话,包括自己和子树(view hierarchy)的 layout 状态等;
2、RenderServer 解析提交的子树状态,生成绘制指令;
3、GPU执行绘制指令;
4、显示渲染后的数据;

上面的 Commit Transaction 其实可以细分为 4 个步骤: Layout、Display、Prepare、Commit

Tile-Based 渲染

Tiled-Based 渲染是移动设备的主流。整个屏幕会分解成N*Npixels组成的瓦片(Tiles),tiles存储于SoC 缓存中。对于每一块 tile,把必须的几何体提交到 OpenGL ES,然后进行渲染(光栅化)。完毕后,将 tile 的数据发送回 CPU。

普通的Tile-Based渲染流程
1、CommandBuffer,接受 OpenGL ES 处理完毕的渲染指令;
2、Tiler,调用顶点着色器,把顶点数据进行分块(Tiling);
3、ParameterBuffer,接受分块完毕的tile和对应的渲染参数;
4、Renderer,调用片元着色器,进行像素渲染,处理得到 bitmap,之后存入 Render Buffer;
5、RenderBuffer,存储渲染完毕的像素,供之后的 Display 操作使用;

离屏渲染

普通情况下 GPU 直接将渲染好的内容放入 Framebuffer 中,而离屏渲染需要先额外创建离屏渲染缓冲区 Offscreen Buffer,将提前渲染好的内容放入其中,等到合适的时机再将 Offscreen Buffer 中的内容进一步叠加、渲染,完成后将结果切换到 Framebuffer 中。

离屏渲染时由于 APP 需要提前对部分内容进行额外的渲染并保存到 Offscreen Buffer,以及需要在必要时刻对 Offscreen Buffer 和 Framebuffer 进行内容切换,所以会需要更长的处理时间。并且 Offscreen Buffer 本身就需要额外的空间,大量的离屏渲染可能早能内存的过大压力。
可见离屏渲染的开销非常大,一旦需要离屏渲染的内容过多,很容易造成掉帧的问题。所以尽量避免离屏渲染。

使用离屏渲染原因:
1、一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。比如阴影、圆角等等。
2、处于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。

触发离屏渲染的情况:
1、使用了 layer.mask
遮罩显示的内容是由两层渲染结果叠加,所以必须要利用额外的内存空间对中间的渲染结果进行保存,因此系统会默认触发离屏渲染。

2、模糊特效 UIBlurEffectView
模糊过程分为多步:先渲染需要模糊的内容本身,然后对内容进行缩放,然后分别对内容进行横纵方向的模糊操作,最后一步用模糊后的结果叠加合成,最终实现完整的模糊特效。
使用 UIBlurEffectView ,应该是尽可能小的 view,因为性能消耗巨大。

3、光栅化的 layer.shouldRasterize
把视图的内容渲染成纹理并缓存,可以通过CALayer的shouldRasterize属性开启光栅化。
注意,光栅化的元素,总大小限制为2.5倍的屏幕。
更新内容时,会启用离屏渲染,所以更新代价较大,只能用于静态内容;而且如果光栅化的元素100ms 没有被使用将被移除,故而不常用元素的光栅化并不会优化显示。
圆角、阴影、组透明度等会由系统自动触发离屏渲染,那么打开光栅化可以节约第二次及以后的渲染时间。而多层 subLayer 的情况由于不会自动触发离屏渲染,所以相比之下会多花费第一次离屏渲染的时间,但是可以节约后续的重复渲染的开销。

4、组透明度 layer.allowsGroupOpacity / layer.opacity
CALayer 的 allowsGroupOpacity 属性,UIView 的 alpha 属性等,同于 CALayer opacity 属性。
allowsGroupOpacity = YES,子 layer 在视觉上的透明度的上限是其父 layer 的 opacity。当父视图的layer.opacity != 1.0时,会开启离屏渲染。layer.opacity == 1.0时,父视图不用管子视图,只需显示当前视图即可。

5、需要进行裁剪的 layer ,layer.masksToBounds / view.clipsToBounds
设置 cornerRadius 剪裁圆角时,没有设置 masksToBounds = YES,由于不需要叠加裁剪,此时是并不会触发离屏渲染的。而当设置了裁剪属性的时候,由于 masksToBounds 会对 layer 以及所有 subLayer 的 content 都进行裁剪,这时会触发离屏渲染。

6、添加了投影的 layer ,layer.shadow*
7、绘制了文字的 layer ,UILabel, CATextLayer, Core Text 等

设置圆角+裁剪(cornerRadius+masksToBounds)、透明度+组透明(allowsGroupOpacity+opacity)、阴影等,都是类似的效果,设置后会应用到所有的 subLayer 上,所以 subLayer 处理后,不能立刻丢弃,等待所有 subLayer 处理完成,然后叠加合成,其中就要被保存在 Offscreen buffer 中,这也就触发了离屏渲染。

需要注意的是,重写 drawRect: 方法并不会触发离屏渲染。重写 drawRect: GPU 会等待 CPU 数据计算完成,然后进行 GPU 中的渲染操作,并且需要额外开辟内存空间。这和标准意义上的离屏渲染并不一样,在 Instrument 中开启 Color offscreen rendered yellow 调试时也会发现这并不会被判断为离屏渲染。

性能优化

通过上面的内容,大致了解了图像显示的过程,屏幕上显示的内容,CUP 计算好内容后交给 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次刷新显示到屏幕上。屏幕 60Hz 的刷新率,每秒显示 60 帧画面,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变,这就导致每秒没有显示 60 帧画面,产生了卡顿。
在开发中保持 APP 的流畅使我们追求的目标,可以通过 Instuments 工具,查看显示相关的数据,从而定位问题,优化性能,提升流畅度。

CUP 资源性能优化

GUP 资源性能优化

相对于 CPU 来说,GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。

References

iOS开发-视图渲染与性能优化
iOS Rendering 渲染全解析
iOS 图像渲染原理
iOS 保持界面流畅的技巧
iOS 浅谈GPU及App渲染流程

上一篇下一篇

猜你喜欢

热点阅读