iOS 高刷屏监控 + 优化:从理论到实践全面解析

Posted 字节跳动技术团队

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 高刷屏监控 + 优化:从理论到实践全面解析相关的知识,希望对你有一定的参考价值。

preferredFrameRateRange 帧率限制。(关于此限制下文会有具体介绍)

  1. Metal 渲染 30Hz/60Hz 视频

使用基于 MTKView 进行渲染的播放器,播放源帧率分别为 30Hz/60Hz 的视频文件

并使用以下几种统计口径的帧率指标进行测试:

头文件中描述,CADisplayLink 是一个 ”Class representing a timer bound to the display vsync “。在回调中比较当前帧/前一帧的时间戳,可以计算出上一帧的渲染耗时(ts),其倒数(1/ts)即为当前的实时帧率。

  1. Xcode GPU Report 帧率

Xcode -> Show Debug Navigator -> FPS 中显示的帧率。这个只能统计当前应用直接通过 OpenGL ES 或者 Metal 进行绘制的帧率,例如游戏渲染/视频播放,无法统计 Core Animation 的帧率(众所周知,后者通过 backboardd 进行绘制)。

  1. Instruments Core Animation FPS

Instruments 中 Core Animation FPS 工具所显示的帧率。这个统计的是 Core Animation 的帧率,即 Render Server backboardd 绘制的频率。目前该工具有 BUG 无法显示高于 60 FPS 的帧率。

  1. Instruments Display/VSync 信号频率

Instruments 中 Display 工具所显示的 Surface/VSync 信号时间戳。如下图所示:

  • Display:指对应显示器的单个 Surface 上屏持续的时间,对应 CPU-GPU 管线的渲染频率
  • VSync:指垂直同步信号时间戳,对应屏幕硬件的刷新频率
  • 在 60Hz 屏幕上,ios 设备默认采用双缓冲刷新机制,也就是前帧缓存和后帧缓存。GPU 总是在后帧缓存上进行当前帧的绘制。当 VSync 信号到来时,交换前后帧缓存的指针(Swap FrameBuffer),屏幕刷新显示新的内容。

    而当屏幕以 120Hz 显示内容时,iOS 会切换成三缓冲刷新机制(见上图中三种颜色的 Surface),这减少渲染管线的压力,但同时会增加一定的渲染上屏延迟。

    Metal 应用可以通过设置 -[CAMetalLayer setMaximumDrawableCount:] 为 2 来在 120Hz 屏幕上强制启用双缓冲机制,避免这种延迟。

    如果屏幕显示内容未发生变化,Surface 则不会发生交换,一个 Surface 的 Display 可能持续数个 VSync 间隔,但多余的 VSync 信号依然代表着硬件层额外的屏幕刷新,造成额外的电量消耗。

    不会进行刷新,所以对应这一帧的 Surface 也长时间(数十秒)未被交换下去,Core Animation FPS 的值显示为 0。

    但由于 VSync 信号仍然以 60Hz 的频率持续触发,屏幕此时正在不停重复展示同样的 Frame Buffer,消耗了额外的电量。

    记录每次 RunLoop AfterWaiting -> BeforeWaiting 的间隔

  • 第二行 tick 记录默认配置的 CADisplayLink 回调间的间隔
  • 最下面则是硬件 Display/VSync 事件时序图
  • 可以观察到下述现象,符合我们之前的对 DisplayLink 的认识:

  • 没有卡顿的情况下,VSync 信号和 RunLoop 的唤醒 & CADisplayLink 回调的触发严格一一对应。
  • RunLoop 卡顿,无法处理 Source 1 信号,DisplayLink 回调被延迟到卡顿结束时。
  • 在此过程中 VSync 信号间隔始终保持不变。
  • 可以设置 hint 请求高刷,但并不一定生效,详见下文“动态帧率的应用场景”部分。
  • 显示滑动中内容时,刷新率在 80Hz 左右波动,并且跟随滑动速度变化而变化。快滑时刷新率升高,慢滑时降低。
  • 显示视频时,刷新率和视频帧率维持一致
  • 可以看到 VSync 信号间隔能主动跟随显示内容的渲染帧率的改变而改变。

    字段才可以解锁 120Hz 的刷新率。

    于此同时,在 iOS 15 中,CADisplayLink 等动画相关 API 也新增了一个用于配置偏好帧率的属性:

    卡顿期间,通过改变 VSync 间隔,系统尝试将缓冲区中的 Surface 283Surface 250 延迟上屏,尽量缩短了用户看到静止画面的时长。

    随后,主线程恢复执行,可以看到 DisplayLink 的回调频率很快恢复至卡顿前的高水平。而此时 VSync 信号由于前述卡顿减缓机制的存在频率其实有所降低。此时二者频率并不吻合。

    这和之前播放慢速动画/慢速滑动的情况很相似,由于卡顿加上缓冲机制的存在导致短时间内系统将屏幕的刷新频率降低,但在 CPU 侧依然维持了 DisplayLink 的高速回调,满足了使用方对 preferredFrameRateRange 这一 API 的设置。

    为了进一步分析了这种机制的本质,笔者接下来会尝试逆向分析 iOS 15 中的系统库相关实现的改动。

    行为和 14 相比一定发生了某种变化。

    逻辑的变化的实现,发现 15 和 14 的实现并无区别。使用 LLDB 进行 debug,逐步分析,观察到后续调用函数为 CA::Display::DisplayLink::callback,其关键反汇编代码如下图所示:

    观察反汇编代码可以发现,如果 CA::display_link_will_fire_handler 这个 block 返回了 NO,则这次 VSync 信号回调不会触发后续的 CA::DisplayLink::dispatch_items 调用。

    实际上在 LLDB 中也验证了这点:

    注意上图中的 _CFRunLoopCurrentIsMain 和上图红框代码接近,后续的 blraa 指令看起来很明显是调用了一个 block(上面的 ldr x9 [x8, #0x10] 就是把 invoke 指针从 block 结构体中取出的意思)。tbz 指令中 w0 寄存器为 block 执行的返回值,为 0(即 NO)时跳转至 0x1848dbc08,而 0x1848dbc08 刚好在 dispatch_items 的调用之后,跳过了该调用。

    通过对上图中 blraa 指令 step in,我们发现这个 block 实际上是由 UIKitCore 注册的:

    找到引用了该符号的 UIKit 的私有方法 __UIUpdateCycleSchedulerStart ,反汇编结果也验证了这点。

    同时发现这个 block 的返回值固定为 0x0。

    而同样的 symbol 在之前的 iOS 版本上并不存在,也就是说这个应该是 iOS 15 的变动。换安装了 iOS 15 的非 ProMotion 设备,重走上面的逆向流程发现,该设备的 CA::display_link_will_fire_handler 为 nil,未注册:

    这里 cbz 执行了跳转,说明 x0 为 nil,而 x0 是由 ldr x0, [x8, #0x1c8] 得到。

    可以看到 x0 就是 CA::display_link_will_fire_handler。继续分析之前找到的私有符号 __UIUpdateCycleSchedulerStart 的相关实现,可以知道这是因为在非 ProMotion 设备上 _UIUpdateCycleEnabled 返回了 NO 导致的。

    在返回 NO 的情况下 __UIUpdateCycleSchedulerStart 方法不会执行,CA::display_link_will_fire_handler 也就不会被注册。

    相关的代码,笔者发现这个的改动并不是仅仅影响 DisplayLink 驱动方式那么简单。

    _UIUpdateCycleEnabled 返回 YES 时,UIKit 会在 UIApplicationMain 中执行 _UIUpdateCycleSchedulerStart。分析该函数,发现 _UIUpdateCycleEnabled 启用时会调用 [CATransaction setDisableRunLoopObserverCommits:YES]

    Core Animation 是绝大部分 iOS 应用的渲染引擎,熟悉 iOS 渲染流程的同学想必都知道它的执行也是由 MainRunLoop 驱动,大致为:

    1. MainRunLoop 因为用户操作/Timer/GCD 等被唤醒,派发相应的事件/回调

    2. 回调中应用修改 Layer Tree,触发 setNeedsLayoutsetNeedsDisplay

    3. MainRunLoop 即将完成本次执行,在即将休眠前向 Observer 派发 BeforeWaiting 事件

    4. BeforeWaiting 中触发 Core Animation 注册的 MainRunLoop Observer,触发事务提交 CA::Transaction::commit()

    5. 自顶向下触发各种 Layout/Display 等逻辑,更新布局/内容
    6. Core Animation 将更新后的 Layer Tree 打包发送给 Render Server
    7. 随后 MainRunLoop 进入休眠

    8. Render Server 将打包好的 Layer Tree 解码,生成并提交对应的 draw calls

    9. GPU 执行渲染指令,渲染出 FrameBuffer,待后续 VSync 信号来临时上屏展示

    上图中 +[CATransaction setDisableRunLoopObserverCommits:YES] 这个调用给了笔者提示,让我们验证一下 CA::Transaction::commit() 在 iOS 15 ProMotion 设备上的执行时机,会发现确实不再由 BeforeWaiting 事件驱动了:

    实际上同样的 Source 0 信号同时也驱动了 CADisplayLink 的回调:

    关注这个 Source 0 的回调符号 runloopSourceCallback,会发现这个 Source0 是由 signalChanges 函数驱动:

    signalChanges 又是由多个回调所驱动:

    其中:

    1. runloopObserverCallback 为一个 BeforeWaitingMainRunLoop observer 驱动。
    2. runloopTimerCallbackmk_timer 驱动,对应的 mach_port 不明,测试发现其回调频率在 1Hz 左右,但也会不断变化,猜测是某种系统计时器。
    3. inputGroupSignaledCallbackmk_timer 驱动,对应的 mach_port 正是 VSync 信号。
    1. requestRegistrySignaledCallbackUIScrollView 在即将开始滑动时驱动。

    通过上面的分析,笔者有理由认为在 iOS 15 上应用的渲染驱动机制出现了比较大的变化。其中之一便是 DisplayLink 的驱动源的改变。

    的情况。换句话说,在屏幕以 120Hz 刷新时,对于丢 1 帧的情况也认为不丢帧,因为此时两帧之间的间隔仍然小于 16.67ms,理论上用户感知不大。

    优点

  • 方案简单,仅需设置 preferredFramesPerSecond 为固定值 60 即可
  • 兼容之前的指标。依然可以计算 FPS 指标,对于刷新率高于 60Hz 的情况统一认为刷新率为 60Hz
  • 缺点

  • 由于只能监控最高 60Hz 的情况,无法评估更高刷新率下一些微小丢帧对用户体验带来的影响,也无法评估对高刷屏的一些优化所带来的技术影响
  • 在低刷新率时,MainRunLoop 依然会以 60Hz 运行,对功耗有一定影响
  • 的 Source 1 信号的回调,使用它来准确监听 VSync 信号,实现对动态帧率的准确监控。

    优点

  • 理论上最精确的监控方案
  • 对功耗的影响最小,回调频率只有在屏幕刷新率实际升高时才会随之提升
  • 缺点

  • 使用了私有 API
  • FPS 指标从此不再适用
  • VSync 信号目前和渲染流程不完全匹配,虽然精确但不一定实用
  • 参数,计算得到当前屏幕的实时刷新率,并修改 preferredFrameRateRange 来进行跟踪。

    优点

    方案相对简单,只需在每次回调中更新 DisplayLink 对象的 preferredFrameRateRange 属性即可

    缺点

  • 由于动态帧率的存在,FPS 指标可以反映实时屏幕刷新情况,但是聚合后的意义不大,消费时需要区分特定机型/场景
  • 观察到目前的最小回调频率为 60Hz,也就是说无法确认 ProMotion 屏幕在 48Hz、30Hz 甚至更低刷新率下的表现
  • 在低刷新率时,MainRunLoop 依然会以 60Hz 运行,对功耗有一定影响
  • 需要注意的是,CADisplayLink 的 preferredFrameRateRange 需要以类似一下格式进行设置:

    这一概念,并着重说明了它比单纯的 FPS 更能适配不同刷新率的场景。

    在 XCTest 框架中,苹果提供了 API XCTOSSignpostMetric 帮助开发者在单测中即时地获取该指标,但相关 API 尽在单测中提供,线上无法使用。而 MetricKit 中的 MXAnimationMetric 尽管可以在线上获取,但却不是实时的,无法满足大型 App 对不同场景的监控需求。

    因此,遵循下面 Apple 对 Hitch Ratio 的定义:

    Hitch time:

  • Time in ms that a frame is late to display.
  • Hitch time ratio:

  • Hitch time in ms per second for a given duration.
  • 笔者尝试实现了基于 CADisplayLink 的 (Scroll) Hitch Time Ratio 的计算方案:

    1. 计算上一帧的帧时间戳与上上一帧的目标帧时间戳得到上一帧的 Hitch Time
    2. 确定该帧是否是在滑动中渲染
    3. 累计得到整体的 Hitch Frame,与累积的帧间隔相比,得到 (Scroll) Hitch Time Ratio
    true 也无法稳定以满帧率滑动(经过验证,这一点在 iOS 15.4 beta 系统上依然成立)。

    通过利用 iOS 15 引入的新 API,我们可以在关键场景如滑动、转场、动画过程中主动解锁更高/限制更低的动态帧率,从而优化流畅度或者优化功率,提升用户体验目标。

    为 120,然后将其添加到 UITrackingRunLoopMode 中。
    中,分别在开始/停止滑动时启用/暂停 CADisplayLink,并修改对应的 preferredFramesPerSecond等属性,触发帧率变化。
                            CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopEntry | kCFRunLoopExit, YES, 0,
    ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity)
    if (activity == kCFRunLoopEntry)
    dp.paused = NO;
    dp.preferredFramePerSecond = 120;
    else
    dp.paused = YES;
    dp.preferredFramePerSecond = 0;

    ), (__bridge CFStringRef)UITrackingRunLoopMode);

    在实践中,由于也存在需要在非滑动状态下解锁帧率上限的情况,所以方案 2 的通用性会更好。

    动画帧率的 API,设置 CAAnimation.preferredFrameRateRange 即可改变其对屏幕刷新率的影响。

  • 对于用户感知明显的,如转场动画,可以设置为 120Hz。
  • 对于感知不明显的,如旋转动画,可以降低其帧率,比如设置为 30Hz。
  • 但是,和 DisplayLink 相同,过上述 API 的设置虽然会“影响”系统的动态帧率的选择,但这种影响并不是绝对的。在实际使用中,笔者发现屏幕选择的刷新率和 CAAnimation 在屏幕上变化的速度有关。

    关于此点,以 iPhone 13 Pro 为例,笔者使用了一个简单的、偏好帧率为固定 120Hz 平移动画进行说明:

    变量为平移的速度,单位为 pt/s,试验发现:

  • speed 取 (0, 160] 时,屏幕刷新率为 60Hz
  • speed 取 [161, 320] 时,屏幕刷新率为 80Hz
  • speed 取 [321, +∞) 时,屏幕刷新率为 120Hz
  • 笔者仅在 iPhone 13 Pro 上测试了平移动画的场景,以上数据仅供参考。

    最后,对于其他的常见的动画 API,例如 UIView.animateWithDurationUIViewPropertyAnimator 等,则没有提供对应 API 进行修改。理论上也可以通过某些手段拿到这些上层 API 所创建的 CAAnimation 对象来实现修改。

    属性来实现,其实现和通过监听 RunLoop 来修改滑动帧率基本相同。

    UIGestureRecognizer 常被用于实现的交互式动画。经过测试,发现在触发手势回调的同时启用一个解锁了频率的 CADisplayLink 也可以间接提高 UIGestureRecognizer 的回调频率,从而实现更高帧率的交互动画。

    对于转场的场景,一个简单的方案是 swizzle UIViewController 的生命周期消息,在出现/消失的节点启用/停用 CADisplayLink 帧率的解锁,从而实现通用的页面转场动画帧率解锁方案

    Flutter 官方也计划提供类似 API 让应用侧可以针对不同的场景(滑动、动画 etc)动态切换屏幕刷新率:https://github.com/flutter/flutter/issues/90675

    咨询相关信息或者直接发送简历内推!

      点击“阅读原文”了解岗位详情

    高刷屏是什么?

    Python一个有趣的彩蛋小恐龙跑酷的黑白像素小游戏,各位都可以打到多少分?有朋友私信说,玩这个需要一个高刷屏,嗯,什么是高刷屏?借此机会,科普一下。

    高刷屏是指拥有高刷新率的屏幕。刷新率是指电子束对屏幕上的图像重复扫描的次数,刷新率越高,所显示的图像(画面)稳定性就越好,刷新率高低将直接决定其价格。

    刷新率分为垂直刷新率和水平刷新率,一般提到的刷新率通常指垂直刷新率。

    垂直刷新率表示屏幕的图像每秒钟重绘多少次,也就是每秒钟屏幕刷新的次数,以Hz(赫兹)为单位。

    例如大苹果13 Pro Max支持120Hz自适应刷新率,在手机的领域,应该就算很高了,

    普通视频的帧率一般都是在每秒25帧,而之前手机的帧率都是传统的60Hz,也就是每秒60帧。简单来说,60Hz就是能够一秒内显示60张画面,而90Hz则是能够在一秒内显示90张画面,120Hz和144Hz也是同理。相比之下,最顶级的144Hz屏幕刷新率比传统的60Hz屏幕的显示画面快了整整2.4倍,并且这个屏幕刷新率决定了显示画面的流畅性,还有细腻程度。

    那么他们究竟有什么区别?

    首先,在60Hz的屏幕上,滑动时有着很明显的延迟、拖影、顿挫感以及滑动时掉帧的感觉。而在另一方的120Hz屏幕上看,你会感觉十分流畅,并且还有着一种很自然、丝滑的视觉体验。

    一般高的屏幕刷新率对我们的视觉来说,是一种更好的体验,只有在你真正使用过后,才能够明显的感觉到。如果日常使用还是感觉没有什么区别,那么不妨把电视剧放慢一点看,或者快速看,就能对比出来。这就是为什么很多用户都表示用过高刷屏就用不回普通的60Hz屏幕了。

    近期更新的文章:

    "万一免五"是什么?

    最近碰到的问题

    Python一个有趣的彩蛋

    国内首个违反GPL的案件介绍

    几种常见的软件开源协议介绍

    文章分类和索引:

    《公众号800篇文章分类和索引

    以上是关于iOS 高刷屏监控 + 优化:从理论到实践全面解析的主要内容,如果未能解决你的问题,请参考以下文章

    高刷屏是什么?

    iOS14意外泄露iPhone高刷开关

    iPhone13下周三发布,提前看剧透:刘海缩小120Hz高刷屏Mini又续一年…

    Web性能优化之 “直出” 理论与实践总结

    手淘 Android 帧率采集与监控详解

    淘宝 Android 帧率采集与监控详解

    (c)2006-2019 SYSTEM All Rights Reserved IT常识