0%

Advanced Graphics and Animations for iOS Apps

本文是对 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 的管道图:

Animation Pipeline1

我们看到在应用程序(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 。如下图所示

Animation Pipeline2

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

动画分为三个阶段,前面两个阶段在应用程序,最后一个在渲染服务器,如下图所示

Animation Process

视图的不同的是,这里提交的不是视图层级,而是动画。这是出于效率的原因,方便我们可以继续更新动画,因为如果提交视图层级的话,动画一更新,又得返回到应用程序提交新的视图层级,很耗时。

Rendering Concepts

来了解渲染的一些概念。

Tile Based Rendering

“first tile based rendering is how all GPUs work.” 基于图块的渲染是 GPU 的工作方式。

Tile Based Rendering

  • 屏幕被分割成 N*N 个像素块,就像之前讲 Points vs Pixels 中的例子一样;
  • 每块都适应 Soc 缓存。(Soc: 苹果 A9 是一款由苹果公司设计的系统芯片(Soc)。可以理解为系统芯片。 维基百科上面写的,这个芯片是 2015.9.9 才首次发布)。
  • 几何体被分割成瓷砖桶(tile buckets),这一步发生在 tiler stage (后面有提到)。这里举了 iPhone icon 的例子,从上图中可以看到,这个 icon 被分割成多个很小的三角形,使得这些三角形块可以单独的渲染,分割这样做的思路可以决定哪一块显示,哪一块渲染。 因为每个像素只有一个像素着色器,所以混合的话还是有问题的,涉及到覆盖绘制。
  • 几何体提交后,光栅化才开始。(所以光栅化能提升性能,因为几何体都提交了,下次渲染的时候就可以省略这一步。)

Rendering pass

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

举了一个渲染遮罩的例子,步骤如下图:

Masking Rendering Pass

分三步走,两步渲染,一步合成。

  1. 将遮罩层(相机 icon)渲染到纹理(texture)上;
  2. 将内容层渲染到纹理上;
  3. 将遮罩添加到内容纹理上。

(texture: 材质贴图,又称纹理贴图,在计算机图形学中是把存储在内存里的位图包裹到 3D 渲染物体的表面。可以把它理解成图片。)

UIBlurEffect

UIBlurEffect 是 iOS8 新出的用来实现模糊效果的类。它的渲染过程如下:

UIBlurEffect Rendering Pass

再看下图,聚焦在一帧,

UIBlurEffect1

我们可以看到有三行,每一行代表一个事件

  • 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)。

  1. content pass, 在这种情况下,它只是一个简单的图像,因此如果我们涉及 UI ,可能需要更长的时间;
  2. downscale, 它实际上相当快。这几乎是不变的成本;
  3. horizontal blur, 也非常快,因为是小区域。
  4. vertical blur, 同上
  5. upscale and tint the blur

我们注意到下图,每个步骤之间的间隙,用橘色标记了

UIBlurEffect2

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 Rendering Pass

比 UIBlurEffect 多了两个步骤,最后一个步骤 filter 是最昂贵的,所以作用区域越小越好,千万别作用到全屏上。
UIVibrancyEffect1

所以也会比 UIBlurEffect 多两个间隙,所以总共 0.6~1.2ms.
UIVibrancyEffect2

Profiling tools

性能调查要考虑以下点

Performance Investigation Mindset

  • 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
2
3
4
5
CALayer *imageViewLayer = cell.imageView.layer;
imageViewLayer.shadowColor = [UIColor blackColor].CGColor;
imageViewLayer.shadowOpacity = 1.0;
imageViewLayer.shadowRadius = 2.0;
imageViewLayer.shadowOffset = CGSizeMake(1.0, 1.0);

请用更高效的

1
imageViewLayer.shadowPath = CGPathCreateWithRect(imageRect, NULL);

因为 Core Animation 必须知道阴影的形状和位置,所以它用 offscreen pass 去渲染内容,在查看刚刚渲染的 alpha channel 来找出阴影的位置。

Round

一般情况下是用以下代码,但是不要用

1
2
3
CALayer *imageViewLayer = cell.imageView.layer;
imageViewLayer.cornerRadius = imageHeight / 2.0;
imageViewLayer.masksToBounds = YES;

下面的方式可能会更高效:

  • 不要在渲染的时候使用 mask ,提前生成圆头像
  • 如果上面的方式做不到,可以在头像上面盖一个中间透明的视图,尽管增加 GPU blend 的工作,但是还是会比离屏渲染快,所以还是可行的。

优化后设备利用率(Device Utilization)是 30% ,之前有 80%,但是没有达到 100% ,这是因为当有离屏渲染时,GPU 必须得有来回切换的空闲时间,所以知道尽管受到 GPU 的限制,由于 offscreen passes 的存在,利用率都达不到 100% 。