本文是对 Advanced Graphics and Animations for iOS Apps 的一个学习记录文章,字幕在 transcripts ,当然也可以下载 WWDC 在桌面上看带有字幕的视频。这篇挺实用的,讲解了渲染的基本流程,以及怎么发现并解决渲染性能的问题。(ps: 20中旬发现这个视频下架了,可以在 419_hd_advanced_graphics_and_animation_performance.mov 下载)。
Core Animation Pipeline
我们知道 Core Animation 是 iOS 上可用的图形渲染和动画基础结构,它将大部分实际绘图工作交给图形硬件以加速渲染(摘自官方文档Core Animation Programming Guide)。 我们先来看看 Core Animation 的管道图:
我们看到在应用程序(Application)和渲染服务器(Render Server)中都有 Core Animation ,但是渲染工作并不是在应用程序里(尽管它有 Core Animation)完成的。它只是将视图层级(view hierarchy)打包(encode)提交给渲染服务器(一个单独的进程,也有 Core Animation), 视图层级才会被渲染。(“The view hierarchy is then rendered with Core Animation with OpenGL or metal, that’s the GPU.”) 大致流程如下:
- Handle Events: 它代表 touch, 即一切要更新视图层级的事情;
- Commit Transaction: 编码打包视图层级,发送给渲染服务器;
- Decode: 渲染服务器第一件事就是解码这些视图层级;
- Draw Calls: 渲染服务器必须等待下一次重新同步,以便等待缓冲区从 它们实现渲染的显示器 返回,然后最终开始为 GPU 绘制,这里就是 OpenGL or metal 。
- Render: 一旦视图资源可用, GPU 就开始它的渲染工作,希望在下个重新同步完成,因为要交换缓冲区给用户。
- Display: 显示给用户看。
在上述情况下,这些不同的步骤总共跨越三帧。在最后一个步骤 display 后,是可以平行操作的,在 Draw call 的时候可以处理下一个 handler event 和 Commit Transaction 。如下图所示
Commit Transaction
先聚焦 Commit transaction 这个阶段,因为这是开发者接触最多的,主要有四个阶段,如下图所示
- Layout: Set up the views. (重载的
layoutSubviews
方法会在这个阶段被调用;视图的创建,被添加到视图层级上;计算内容,比如:字符串,用来布局 label ;这个阶段通常是 CPU 或者 I/O 限制,所以做的事情要轻量) - Display: Draw the views. (主要是 core graphics 用来绘制,调用重载的
drawRect:
方法来绘制,绘制字符串;这个阶段通常是 CPU 或者内存限制,所以减少 core graphics 的工作) - Prepare: Additional Core Animation work. (主要是图片解码和图片转换。所以,图片大小和格式都是被 GPU 支持的,不然转换是发生在 CPU 上的,最好是 index bitmap ,可以免去转换)
- Commit: Package up the layers and send them to the render server. (视图层级不要太复杂,尽量扁平,因为这里的打包是循环处理的)
Animation
动画分为三个阶段,前面两个阶段在应用程序,最后一个在渲染服务器,如下图所示
跟视图的不同的是,这里提交的不是视图层级,而是动画。这是出于效率的原因,方便我们可以继续更新动画,因为如果提交视图层级的话,动画一更新,又得返回到应用程序提交新的视图层级,很耗时。
Rendering Concepts
来了解渲染的一些概念。
Tile Based Rendering
“first tile based rendering is how all GPUs work.” 基于图块的渲染是 GPU 的工作方式。
- 屏幕被分割成 N*N 个像素块,就像之前讲 Points vs Pixels 中的例子一样;
- 每块都适应 Soc 缓存。(Soc: 苹果 A9 是一款由苹果公司设计的系统芯片(Soc)。可以理解为系统芯片。 维基百科上面写的,这个芯片是 2015.9.9 才首次发布)。
- 几何体被分割成瓷砖桶(tile buckets),这一步发生在 tiler stage (后面有提到)。这里举了 iPhone icon 的例子,从上图中可以看到,这个 icon 被分割成多个很小的三角形,使得这些三角形块可以单独的渲染,分割这样做的思路可以决定哪一块显示,哪一块渲染。 因为每个像素只有一个像素着色器,所以混合的话还是有问题的,涉及到覆盖绘制。
- 几何体提交后,光栅化才开始。(所以光栅化能提升性能,因为几何体都提交了,下次渲染的时候就可以省略这一步。)
Rendering pass
如上图所示,我们假设视图层级已经被提交到渲染服务器,并且 Core Animation 已经解码它,现在需要用 OpenGL 或者 metal 去渲染了,文章讲师举例是用的 OpenGL (所以这里的 Slide 比前面讲 Core Animation Pipeline 的 Slide 在 Render Server 这一栏,多了 OpenGL 在里面)。具体流程如下:
- GPU 收到 Command Buffer ;
- 顶点着色器开始运行,思路就是先将所有的顶点转换到屏幕空间,然后平铺处理,平铺成**瓷砖桶(tile bucket)**的几何图形(这里分两步走,先顶点处理然后平铺,统称为 Tiler stage, 在 Instrument 的 OpenGL ES tiler utilization 能看到这一步。)这一步的产出被写入 Parameter Buffer, 下一阶段不会马上启动。相反,会等待,直到 a. 处理完所有的几何体,并且都位于 Parameter Buffer 中;或者 b. Parameter buffer 已满(满了的话,必须刷新它)。
- 像素着色器处理,这一步被称为 Renderer stage, 产出被写入 Render Buffer 。(在 Instrument 的 OpenGL ES renderer utilization 能看到这一步。)
Masking Rendering pass Example
举了一个渲染遮罩的例子,步骤如下图:
分三步走,两步渲染,一步合成。
- 将遮罩层(相机 icon)渲染到纹理(texture)上;
- 将内容层渲染到纹理上;
- 将遮罩添加到内容纹理上。
(texture: 材质贴图,又称纹理贴图,在计算机图形学中是把存储在内存里的位图包裹到 3D 渲染物体的表面。可以把它理解成图片。)
UIBlurEffect
UIBlurEffect 是 iOS8 新出的用来实现模糊效果的类。它的渲染过程如下:
再看下图,聚焦在一帧,
我们可以看到有三行,每一行代表一个事件
- tile activity
- render activity
- VBlank interrupt, “and the last row I put in the VBlank interrupt and we can actually see what our frame boundaries are.” (我们实际上可以看到我们的帧边界是什么)
然后我们看看每个渲染步骤所需的时间,每个渲染步骤都牵扯到了上面所提到的事件(tile/render/VBlank interrupt)。
- content pass, 在这种情况下,它只是一个简单的图像,因此如果我们涉及 UI ,可能需要更长的时间;
- downscale, 它实际上相当快。这几乎是不变的成本;
- horizontal blur, 也非常快,因为是小区域。
- vertical blur, 同上
- upscale and tint the blur
我们注意到下图,每个步骤之间的间隙,用橘色标记了
5 个步骤中间有 4 个间隙,之所以存在,是因为这是发生在 GPU 上切换所花的时间。在空闲时间,每个步骤所花费的时间大概在 0.10.2ms, 所以总共 0.40.8ms, 所以这个是 16.67ms 的一个重要组成部分。
还列举了不同设备间的耗时,有一种设备某个 Dark style 下的时间是 18.15ms, 超过 16.67ms, 所以不可能在 60 hert 渲染完成。所以 Apple 在这些设备上不支持 blur 。
UIBlurEffect 有三种 style: Extra light, Light, Dark ,它们消耗的资源各不相同, Dark 最少, Extra light 最多。
UIVibrancyEffect
UIVibrancyEffect 是在模糊之上使用的效果,它可以确保内容突出,而不会被模糊。它的渲染过程如下:
比 UIBlurEffect 多了两个步骤,最后一个步骤 filter 是最昂贵的,所以作用区域越小越好,千万别作用到全屏上。
所以也会比 UIBlurEffect 多两个间隙,所以总共 0.6~1.2ms.
Profiling tools
性能调查要考虑以下点
- What is the frame rate? Goal is always 60 fps. // 检查工具: Core Animation template / OpenGL ES driver template
- CPU or GPU bound? Lower utilization is desired and saves battery. (更少的 CPU 或者 GPU 利用率,让电池更持久。) // 检查工具: OpenGL ES driver template
- Any unnecessary CPU rendering? GPU is desirable but know when CPU makes sense. (得知道渲染什么和怎么渲染,
drawRect
方法尽量少用,减少让 CPU 的工作,让 GPU 做更多的渲染。) // 检查工具: Core Animation template / OpenGL ES driver template - Too many offscreen passes? Fewer is better. (前面说 UIBlurEffect 的时候有说到,橘色的间隙就是用在 GPU 切换时间,每个间隙大概 0.1~0.2ms 。 离屏渲染也会出现这样的情况,因为它必须进行切换,所以得减少。因为前面有提到,我们减少 CPU 或者 GPU 的使用时间。) // 检查工具: Core Animation template
- Too much blending? less is better. (GPU 处理 blending 合成的时候,操作昂贵,消耗性能) // 检查工具: Core Animation template
- Any strange image formats or sizes? Avoida on-the-fly conversions or resizing. (会转给 CPU 去处理,增加 CPU 的负担) // 检查工具: Core Animation template
- Any expensive views or effects? Understand the cost of what is in use. (避免昂贵的效果,例如 Blur 和 Vibrancy ,得去考量。) // 检查工具: Xcode view debugging
- Anything unexpected in the view hierarchy? Know the actual view hierarchy. (添加和移除要匹配。) // 检查工具: Xcode view debugging
检查工具
上面每个例子后面都有提到一个检测工具,这里来讲讲相应检测工具的作用。请注意一点,在开始挖掘代码以试图找出正在发生的事情之前,这总是一个很好的起点(先看大概发生什么问题,再深入研究代码)。
Core Animation template
- 看 fps
- color blended layers, green 表示不透明, red 代表需要去 blend 混合。 增加 GPU 的工作。 绿多红少,是理想中的状态。
- color hit screens and misses red, 展示如何使用或滥用 CALayer’rasterize 属性,没命中缓存就是红色。第一次启动会有很多红色,因为必须在它被缓存之前渲染一次,后面就没有了,因为缓存了。
- color copied images, 如果是 GPU 不支持的图片就会让 CPU 去转换(在 commit phase),增加了 CPU 的工作。 显示为蓝绿色(cyan)就表示让 CPU 去转换,影响滚动体验。 所以 size and color/image format 最好提前在后台处理好,不要阻塞主线程。
- color misaligned images, 黄色表示需要缩放,紫色表示像素没对齐。
- color offscreen-rendered yellow, 黄色代表离屏渲染。 nav bar 和 tool bar 是黄色,因为这些图层的模糊实际上模糊了它背后的内容(前面 blur 有讲过)。
- color OpenGL fast path blue, 蓝色是好事,由显示硬件去 blend ,这样就会减少 GPU 的工作。
- flash updated regions, 正在更新的部分为黄色。 理想状况下,黄色区域越少越好。它意味着 CPU 和 GPU 的工作都减少了。
OpenGL ES driver template
- device utilization, which will show you how much the GPU is in use during the trace. (使用率越少越好,这里举例的是 30% vs 70%(心中的理想值))
- render and tiler utilization, correspond to the renderer and tiler phases.
- CoreAnimationFramesPerSecond, what the actual frame rate is that we’re seeing.
Time Profiler template
- 看调用栈耗时,看 CPU 在干什么;
Case studies
讲了两个例子(Fictitious Photo/Contacts Application),在旧设备上性能较差,都是 offscreen render 导致的,并且还是项目中常用的设置阴影和设置圆角。要在不同的设备上测试,尽管新设备没问题,可能旧设备就有问题。
Shadow
一般情况下是用以下代码,但是不要用
1 | CALayer *imageViewLayer = cell.imageView.layer; |
请用更高效的
1 | imageViewLayer.shadowPath = CGPathCreateWithRect(imageRect, NULL); |
因为 Core Animation 必须知道阴影的形状和位置,所以它用 offscreen pass 去渲染内容,在查看刚刚渲染的 alpha channel 来找出阴影的位置。
Round
一般情况下是用以下代码,但是不要用
1 | CALayer *imageViewLayer = cell.imageView.layer; |
下面的方式可能会更高效:
- 不要在渲染的时候使用 mask ,提前生成圆头像
- 如果上面的方式做不到,可以在头像上面盖一个中间透明的视图,尽管增加 GPU blend 的工作,但是还是会比离屏渲染快,所以还是可行的。
优化后设备利用率(Device Utilization)是 30% ,之前有 80%,但是没有达到 100% ,这是因为当有离屏渲染时,GPU 必须得有来回切换的空闲时间,所以知道尽管受到 GPU 的限制,由于 offscreen passes 的存在,利用率都达不到 100% 。