帧率是咱们衡量应用流畅度的一个重要基准目标。本文将简略介绍 Android 线上帧率核算计划的演进和业界根据帧率来衡量卡顿的相关目标设计。

帧率核算计划的演进

Choreographer.postFrameCallback

自 Android 4.1 引进 Choreographer 来调度 UI 线程的制作相关使命之后,咱们便有了一个简略衡量 UI 线程制作效率的计划:经过继续调用 Choreographer 的 postFrameCallback 办法来得到根据 VSync 周期的回调,根据回调的距离或许办法参数中的当时帧开始时刻 frameTimeNanos 来核算帧率( 注意到根据回调距离核算帧率的状况,因为 postFrameCallback 注册的回调类型是 Animation,早于 Traversal 但晚于 Input,实践回调的起点并不是当时帧的起点 )。

这一计划简略牢靠,但要害问题在于 UI 线程通常都不会不间断地履行制作使命,在不需要履行制作使命( scheduleTraversals )时,UI 线程原本是不需要恳求 VSync 信号的,而继续调用 postFrameCallback 办法的状况,则会连续恳求 VSync 信号,使得 UI 线程始终处于比较活泼的状况,一起核算得到的帧率数据实践也会包含不需要制作时的状况。精确来说,这一计划实践衡量的是 UI 线程的繁忙程度。

Matrix 前期计划

怎样能得到真正的帧率数据呢?腾讯 Matrix 的前期完成上,结合 Looper Printer 和 Choreographer 完成了一个比较巧妙的计划,做到了核算真正的 UI 线程制作使命的细分耗时。

具体来说,根据 Looper Printer 做到对 UI 线程每个音讯履行的监控,一起反射给 Choreographer 的 Input 回调行列头部刺进一个使命,用来监听下一帧的起点。队头的 Input 使命被调用时,说明当时所在的音讯是处理制作使命的,则音讯履行的结尾也便是当时帧的结尾。一起,在队头的 Input 使命被调用时,给 Animation 和 Traversal 的回调行列头部也刺进使命,如此总共得到了四个时刻点,能够将当时帧的耗时细分为 Input,Animation 和 Traversal 三个阶段。在当时处理制作使命的音讯履行完后,从头注册一个 Input 回调行列头部的使命,便能够继续监听下一帧的耗时状况。

这一计划没有运用 postFrameCallback( 不自动恳求 VSync 信号 ),防止了前一个计划的问题,但整体计划上偏 hack,或许存在兼容性问题( 实践 Android 高版别上对 Choreographer 的内部回调行列确实有所调整 )。此外,当时计划也会受到上一计划的搅扰,假如存在其他事务在继续调用 postFrameCallback,也会使得这里核算到的数据包含非制作的状况。

JankStats 计划

实践在 Android 7.0 之后,官方已经引进了 FrameMetrics API 来供给帧耗时的具体数据。androidx 的 JankStats 库首要便是根据 FrameMetrics API 来完成的帧耗时数据核算。

在 Android 4.1 – 7.0 之间,JankStats 仍然是根据 Choreographer 来做帧率核算的,但计划和前两者均不同。具体来说,JankStats 经过监听 OnPreDrawListener 来感知制作使命的产生,此刻,经过反射 Choreographer 的 mLastFrameTimeNanos 来获取当时帧的开始时刻,再经过往 UI 线程的音讯行列头部抛使命的方法来获取当时帧的 UI 线程制作使命结束时刻( 在支撑异步音讯状况下,将该音讯设置为异步音讯,尽量确保获取结束时刻的使命紧跟在当时使命之后 )。

这一计划简略牢靠,而且得到确实实是真正的帧率数据。

在 Android 7.0 及以上版别,JankStats 则直接经过 Window 的新办法 addOnFrameMetricsAvailableListener,注册回调得到每一帧的具体数据 FrameMetrics。
FrameMetrics 的数据核算具体是怎样完成的?简略来说,Android 官方在整个制作烘托流程上都做了打点来支撑 FrameMetrics 的数据核算逻辑,具体包含了

  • 根据 Choreographer 记录了 VSync,Input,Animation,Traversal 的开始时刻点
  • 根据 ViewRootImpl 记录了 Draw 的开始时刻点( 结合 Traversal 区别开了 MeasureLayout 和 Draw 两段耗时 )
  • 根据 CanvasContext( hwui 中 RenderThread 的烘托流程 )记录了 SyncDisplayList,IssueDrawCommand,SwapBuffer 等时刻点,Android 12 上更是进一步支撑了 GPU 上的耗时核算

能够看到,FrameMetrics 供给了以往计划难以给到的具体分阶段耗时( 特别注意 FrameMetrics 供给的数据存在系统版别间的差异,具体的数据处理能够参阅 JankStats 的内部完成 ),而且在内部完成上,相关数据在制作烘托流程上默许便会被核算( 即使咱们不注册监听 ),根据 FrameMetrics 来做帧率核算在数据采集阶段带来的额定性能开销微乎其微。

帧率相关目标设计

简略的 FPS( 均匀帧率 )数据并不能很好的衡量卡顿。在能够精确采集到帧数据之后,怎样根据采集到的数据做进一步处理得到更有实践价值的目标才是更为要害的。

Android Vitals 计划

Android Vitals 本身只界说了两个目标,根据单帧耗时简略区别了卡顿问题的严重程度。将耗时大于 16ms 的帧界说为慢帧,将耗时大于 700ms 的帧界说为冻帧。

JankStats 计划

JankStats 的完成上,则默许将单帧耗时在希望耗时 2 倍以上的状况视为卡顿( 即满帧 60FPS 的状况,将单帧耗时超越 33.3ms 的状况界说为卡顿 )。

Matrix 计划

Matrix 的目标设计则在 Android Vitals 的基础上做了进一步细分,以掉帧数为量化目标( 即满帧 60FPS 的状况,将单帧耗时在 16.6ms 到 33.3ms 间的状况界说为掉一帧 ),将帧耗时细化为 Best / Normal / Middle / High / Frozen 多类状况,其间 Frozen 实践对应的便是 Android Vitals 中的冻帧。

能够看到以上三者都是根据单帧耗时本身而非均匀帧率来衡量卡顿。

手淘计划

手淘的计划则给出了更多的目标。

根据帧率数据本身的,细分场景界说了滑动帧率和卡顿帧率。
滑动帧率比较好了解;
卡顿帧率指的是,在呈现卡顿帧( 界说为单帧耗时 33.3ms 以上 )之后,继续核算一段时刻的帧耗时( 直到达到 99.6ms ( 6 帧 )而且下一帧不是卡顿帧 )来核算帧率,经过独自核算以卡顿帧为起点的细粒度帧率来防止卡顿被均匀帧率掩盖的问题。和前面几个计划比较,卡顿帧率的特色在于一定程度上保留了帧数据的时刻特点,一定程度上能够区别出离散的卡顿帧和连续的卡顿。

根据单帧耗时的,将冻帧占比( 即大于 700 ms 的帧占总帧数的份额 )作为独立目标;此外还有参阅 iOS 界说的 scrollHitchRate,即滑动场景下,预期外耗时( 即每一帧超越希望耗时部分的累加 )的占比。

此外,得益于 FrameMetrics 的具体数据,手淘的计划还完成了简略的自动化归因,即根据 FrameMetrics 的分阶段耗时数据简略判别是哪个阶段导致了当时这一帧的卡顿。