作者| 岚遥
AnimationHitches 的运行原理
背景
在 Xcode12 中,Instrument 新增 AnimationHitches 检测类型用以检测卡顿,并去除 CoreAnimation 检测方式。在支持 PromotionDisplay 的设备上帧率可调整至 120 帧,并且会根据当前用户手势和设备状态进行动态调整。此时再继续使用帧率来判断性能的好坏及流畅度将会是一个错误的选择。所以 AnimationHitches 主要用于代替帧率检测,并且提出 卡顿时间比(Hitch Time Ratio) 的概念用于替代 FPS。由于目前关于 Hitch 相关的资料很少,而在 iPhone13Pro 之前 iPhone 屏幕最高刷新频率仍为 60 HZ,所以很多同学都还未关注到该能力。所以本篇将主要介绍 Hitch(卡顿) 的概念、RenderLoop(渲染循环) 的整体流程,卡顿类型及如何避免卡顿。
什么是卡顿?
▐ 概念
任何时候屏幕上出现晚于预计的帧都属于卡顿。
▐ 实例
例如 滚动动画(Scroll)、点击动画(Animation)、转场动画(Transition),这些流畅的动画构建了一种用户和屏幕内容的视觉连接感,而如果动画卡顿会导致动画画面跳跃,打破这种连接感,用户体验会变得很差。
布局阶段
在布局阶段, layoutSubviews 会被所有需要布局的 View 调用。比如布局视图(frame、bounds、transform),增加或移除视图,亦或是直接调用 setNeedsLayout。注意这些布局操作并非立即执行,系统会合并这些布局请求,在 Runloop 休眠前统一执行这些操作。
显示阶段
在显示阶段,drawRect 会被每个需要被更新的 View 调用。比如 UILabel、UIImageView 或者只是任何重写 drawRect 方法的类。他们必须调用 setNeedsDisplay 用以支持 View 的更新。在绘制时每个自定义的绘图图层都会接收到带纹理的 CoreGraphics 的背景。他们将利用 CoreAnimation 进行绘制,这些图层就变成了图片。所以如果没有必要则不要重写 drawRect 方法,其不仅会额外开辟一块内存用以存储 bitmap,还会在 CPU 上进行绘制,增加了整体主线程时间占用,当自定义 drawRect 视图较多时,对整体的内存压力也比较大。
准备阶段
在 Prepare 阶段还没有解码的图像将会在这一步进行解码,也就是我们需要优化的常见的图片主线程解码操作。
对于每个被解码的图像, App 可能会持续存在大量的内存分配。这种内存分配与输入图像的大小成正比,而与 FrameBuffer 中实际渲染的图像视图的大小没有必然联系。当 App 占用越来越多的内存时,操作系统将会开始压缩物理内存(physical memory)。整个过程都需要 CPU 的参与,所以除了我们自己的 App 对 CPU 的使用外,还可能会增加无法控制的全局 CPU 使用率。最终,我们的 App 可能会消耗更多的物理内存,以至于操作系统需要启动终止进程,它将从低优先级的后台进程开始。如果我们的 App 对内存的消耗了达到了特定数量,可能会被终止,这也就是为什么经常会因为大图的原因产生 OOM。
若某个图像的颜色格式 GPU 无法直接使用,也会在这一步进行格式转换。这就要求对该图像进行 copy 操作,而不是直接使用指针,这样会耗时更长及占用更多的内存。
提交阶段
在提交阶段中,视图树将会被递归打包并发送到 RenderServer 中,所以当视图层级较为复杂时,这个过程耗费的时间也会更长一些,所以需要尽量减轻视图层级结构。
- RenderServer
RenderServer 负责将我们的图层树转换为真正可显示的图像。RenderServer 有两个阶段:Prepare 和 Execute 。在 Prepare 阶段我们的图层树被编译成一系列简单的指令,供 GPU 执行,帧动画也在此处进行处理。在渲染执行阶段 GPU 将 App 的图层绘制成最终图像。
总结
本篇主要讨论了 RenderLoop 以及新的一帧展现给用户的整个流程,并且着眼于什么是卡顿,以及它的两种类型:提交卡顿以及渲染卡顿。并最终定义了卡顿时间比用以测量当前 App 的卡顿程度和性能。相信大家对整个渲染循环和卡顿类型有了更清晰的认识,在日常编码中也可以尽量避免这些问题。
本篇主要介绍了一些原理相关的概念,那么具体的卡顿应该如何测量?下一篇将会通过实践结合 Instrument 的 AnimationHitches 能力分析 DXSDK 作为卡片层面在日常信息流的使用过程中在性能方面存在的一些问题,以及 DXSDK 上半年做的一些性能优化改进。