Article / 文章中心

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

发布时间:2022-01-24 点击数:955


APM  供给帧率的相关数据,即 FPS(Frames Per Second) 数据。FPS 在必定程度上反映了页面流通程度,但 APM 供给的 FPS  并不是很准确。恰逢手淘低端机功能优化项目开启,亟需相关方针来衡量对滑动体会的优化,帧率数据探究实践就此摆开。

在探究实践中,咱们遇到了许多问题:

  1. 高刷手机占比相对不低,影响全体 FPS 数据
  2. 非人为滑动数据参杂在 FPS 中,不能直接表现用户操作体会
  3. 核算均匀数据时,卡顿数据被淹没在海量正常数据中,一次卡顿是否只影响一个 FPS 值仍是一次用户操作体会?

经过一段时刻的探究,咱们沉积下来了一些方针,其间包括:滑动帧率、冻帧占比、scrollHitchRate、卡顿帧率。除了相关帧率方针之外,为了更好的指导功能优化,APM 还供给了帧率主因剖析,一同为了更好的定位卡顿问题,也供给了卡顿仓库。

image.png

下面是 APM 根据渠道的特性,对帧率相关探究实践的详细介绍,期望本文可以给咱们带来一些协助。
体系烘托机制

在介绍方针的完成之前,首要需求了解体系是如何做烘托的,只要知晓体系烘托机制,才干协助咱们更好的进行帧率数据核算处理。

烘托机制是  Android 中重要的一部分,其间又牵扯甚广,包括咱们常说的 measure/layout/draw  原理、卡顿、过度绘制等,都与其相关。在这儿咱们首要是对烘托流程进行全体了解,知晓后续需求核算哪几部分、经过体系 API  得到了哪几部分,以便核算出方针数据。

  烘托流程

image.png

咱们都知道,当触发烘托后,会走到  ViewRootImpl 的 scheduleTraversals。这时,scheduleTraversals  方法首要是向  Choreographer 注册下一个 VSync 的回调。当下一个 VSync 来暂时,Choreographer 首要切到主线程(传  VSync 上来的 native  代码不运转在主线程),当然它并不是直接给 Looper sendMessage,而是  msg.setAsynchronous(true) ,提高了 UI 的响应速率。

当切到主线程后,Choreographer 开端履行一切注册了这个 VSync 的回调,回调类型分为以下四种:

  1. CALLBACK_INPUT,输入事情
  2. CALLBACK_ANIMATION,动画处理
  3. CALLBACK_TRAVERSAL,UI 分发
  4. CALLBACK_COMMIT

Choreographer 会将一切的回调按类型分类,用链表来组织,表头存在一个巨细固定的数组中(因为只支撑这四种回调)。在 VSync 发送到主线程的音讯中,就会一条链表一条链表的取出顺序履行并清空。

而在  scheduleTraversals 注册的便是 CALLBACK_TRAVERSAL 类型的 callback,这个 callback  中履行的便是咱们最为熟悉的 ViewRootImpl#doTraversal() 方法,doTraversal 方法中调用了  performTraversals  方法,performTraversals 方法中最重要的便是调用了耳熟能详的  performMeasure、performLayout、performDraw 方法。

详细代码可以翻看: android.view.Choreographer 和 android.view.ViewRootImpl

从这儿咱们可以看到,想要上屏一帧数据,至少包括:VSync 切到主线程的耗时、处理输入事情的耗时、处理动画的耗时、处理 UI 分发(measure、layout、draw)的耗时。

可是,当 draw 流程完毕,仅仅 CPU 核算部分完毕,接下来会把数据交给 RenderThread 来完成 GPU 部分作业。

  屏幕改写

Android 4.1 引进了 VSync 和三缓冲机制,VSync 给予开端 CPU 核算的机遇,以及 GPU 和 Display 交流的缓冲区的机遇,这样有利于充分运用时刻来处理数据和削减 jank。
image.png

上图中 A、B、C 分别代表着三个缓冲区。咱们可以看到 CPU、GPU、显现器都能尽快拿到 buffer,削减不必要的等候。假如显现器和 GPU 现在都运用着一个 buffer,假如下一次烘托开端了,因为还有一个 buffer 可以用于 CPU 数据的写入,所以可以马上开端下一帧数据的烘托,例如图中第一个 VSync。

是不是引进三缓冲机制就没有任何问题呢,当咱们仔细看上图可发现,数据 A 在第三个 VSync 来暂时就现已准备好,随时可以改写到屏幕上,到实在刷到屏幕却是第四个 VSync 降临。由此可知,三缓冲尽管有效运用了等候 VSync 的时刻,削减了 jank,可是带来了推迟

这儿仅仅简略带咱们回顾了这块的知识,建议咱们翻下发展的历史,知其然亦要知其所以然。

对帧数据信息的挖掘

当咱们知道了整个体系烘托的流程后,咱们需求监控什么,怎样监控,这是一个问题。

  业界计划

image.png

  • APM 原始计划

当收到  Touch 事情后,APM 会收集页面 1s 内 draw 的次数。这个计划的长处是功能损耗低,可是存在致命缺点。假如页面烘托总时长不足 1s  就停止改写,会导致数据人为偏低。其次,触碰屏幕不必定会带来改写,改写也不必定是 Touch 事情带来的。而以上状况核算出来的都是脏数据。

可是,Android 在 ViewRootImpl 完成了一个Debug 的 FPS 计划,原理与上诉计划类似,都是在 draw 时累积时长到 1s,所以,假如是想要一个低本钱功能无损的线下测验 FPS,这不失为一个计划。

感兴趣可以看 ViewRootImpl 的 trackFPS 方法。

  • Matrix

在帧率这部分,Matrix  立异性的 hook 了 Choreographer 的 CallbackQueue,一同还经过反射调用 addCallbackLocked  在每一个回调行列的头部添加了自界说的 FrameCallback。假如回调了这个 Callback,那么这一帧的烘托也就开端了,当时在  Looper 中正在履行的音讯便是烘托的音讯。这样除了监控帧率外,还能监控到当时帧的各个阶段耗时数据。

除此之外,帧率回调和 Looper 的 Printer 结合运用,可以在呈现卡顿帧的时分去 dump 主线程信息,便于事务方处理卡顿,可是频繁拼接字符串会带来必定的功能开支(println 方法调用时有字符串拼接)。

  • 常规

运用 Choreographer.FrameCallback 的 doFrame(frameTimeNanos: Long) 方法,在每一次的回调里核算两帧之差,经过核算可以得到 FPS。

  滑动帧率

FPS 是业界简略而又通用的一个方针,是 Frames Per Second 的简写,即每秒烘托帧数,浅显来讲便是每秒烘托的画面数。

核算出 FPS 并不是咱们的方针,咱们一直期望核算出的是滑动帧率,针对 FPS,咱们更为关注的是用户在交互进程中的帧率,监控这一类帧率才干更好反映用户体会。

首要,面临之前的收集计划,根本不能收集出契合界说的 FPS,所以原始的计划就必须要进行舍弃,需求进行重新规划。当看到 Matrix 的计划时,觉得想法很棒,可是过分 hack,咱们更倾向于维护本钱更低、稳定性高的体系开放 API。

所以,在挑选上,咱们仍是决议运用最一般的 Choreographer.FrameCallback 进行完成。当然,它不是最完美的,可是可以尽量在规划上去避免这种缺点。

那咱们怎样核算出一个 FPS 值呢?

image.png

Choreographer.FrameCallback 被回调时,doFrame 方法都带上了一个时刻戳,核算与上一次回调的差值,就可以将之视之为一帧的时刻。当累加超越 1s 后,就可以核算出一个 FPS 值。

在这个进程中,有个点要咱们知晓,doFrame 在什么机遇回调:


首要,咱们每一次回调后,都需求对  Choreographer 进行 postFrameCallback 调用,而调用 postFrameCallback 便是鄙人一帧  CALLBACK_ANIMATION 类型的链表上进行添加一个节点。所以,doFrame 回调机遇并不是这一帧开端核算,也不是这一帧上屏,而是 CPU 处理动画进程中的一个 callback

当核算出一个 FPS 值后,就需求在上面叠加以下状况了:

  • View 滑动帧率

在最开端完成时,View 只需滑动就监控帧率,一直帧率产出到不滑动停止。根据需求,咱们的帧率收集就变成了如下这样:

image.png

那怎样监控 View 是否有滑动呢?那就需求介绍一下这个 ViewTreeObserver.OnScrollChangedListener。毕竟只要了解完成原理,才干决议是否可用。

// ViewRootImpl#draw private void draw(boolean fullRedrawNeeded) {  // ...  if (mAttachInfo.mViewScrollChanged) {  mAttachInfo.mViewScrollChanged = false;  mAttachInfo.mTreeObserver.dispatchOnScrollChanged();  }  // ...  mAttachInfo.mTreeObserver.dispatchOnDraw();  // ...  }

咱们可以看到,在  ViewRootImpl#draw 中,判断了 mAttachInfo 信息中 View  是否发生了滑动,假如发生滑动就分发出来。那么什么时分设置的 View 位置变化(发生滑动)的呢?在 View 的 onScrollChanged  被调用的时分:

 // View#onScrollChanged  protected void onScrollChanged(int l, int t, int oldl, int oldt) {  // ...  final AttachInfo ai = mAttachInfo;  if (ai != null) {  ai.mViewScrollChanged = true;  }  // ...  }

onScrollChanged 就直接连接着 View#scrollTo 和 View#scrollBy,在大多数场景下,现已满足通用。

根据咱们之前讲解的烘托流程:咱们可以看到  ViewTreeObserver.OnScrollChangedListener 的回调是在 ViewRootImpl#draw 中,那么  Choreographer.FrameCallback 的回调先于  ViewTreeObserver.OnScrollChangedListener 的。

关于单帧,就可以如下表示:

image.png

这样,每一帧都带上了是否滑动的状况,当某一帧是滑动的帧,就可以开端计数,一直累积时刻到 1s,一个滑动帧率数据核算出来就出来了。

  • 手指滑动帧率

View 滑动帧率,在线下验证时,与测验渠道出的数据共同,并且可以契合基本需求,检验经过。上线后,也开端了运转,并可以承担起帧率相关作业。

可是,View 滚动并不代表着是用户操作导致,数据一直不全是用户体会的效果。所以,咱们开端完成手指的滑动帧率。

手指滑动帧率,首要咱们需求可以接纳到手指的 Touch 行为。由于 APM 中已有对 Callback 的 dispatchTouchEvent 接口的 hook,所以决议直接运用此接口识别手指滑动。

image.png

这个时分,咱们需求知道几个机遇问题:

  1. 有 dispatchTouchEvent 不会立马发生 doFrame
  2. 经过 dispatchTouchEvent 核算移动时刻/距离超越 TapTimeout/ScaledTouchSlop,不必定立马发生 doFrame

所以,经过  dispatchTouchEvent 核算移动时刻/距离超越 TapTimeout/ScaledTouchSlop 时,只会给一个  flag,告诉后边的 ViewTreeObserver.OnScrollChangedListener 的 doFrame  可以开端核算成手指滑动帧率。

  • 功能优化/滑动次数识别

咱们在收到每一帧的 doFrame 回调后,都需求重新 postFrameCallback。每一次 postFrameCallback 都会注册 VSync(假如没有被注册),当 Vsync 降临后,会给主线程抛一个音讯,这势必会给主线程带来必定的压力。

众所周知,体系在页面静止的时分是不会进行烘托的,也就不会有 VSync 被注册。那么在没有烘托的时分,是否也需求 post 呢?不需求,没有意义,是可以过滤掉的。根据这个理念,咱们对滑动帧率的核算进行了优化。

image.png

需求削减非必要的帧回调与注册,就需求清晰几个问题:

  1. 起点(什么时分开端 postFrameCallback):在第一次收到 scroll 事情的时分(onSrollChanged)
  2. 结尾(什么时分不再 postFrameCallback):在核算完一个手指滑动 FPS 后,假如下一帧不再滑动,那么就停止注册下一帧的回调。

假如细心的话,就会发现,这儿的起点可以以为是手指带来的滑动的烘托起点,这儿的结尾可以以为是手指带来的滑动的烘托结尾(包括了 Fling),这个数据很重要,咱们相当于识别了一次手指滑动,并且可以供给每次手指滑动的耗时等数据。

这样进行优化是否就完美无缺呢?其实不是的,仔细看上图的核算开端时刻点,就会发现:损失了开端滑动的第一帧数据。因为咱们核算的是两次  doFrame 回调的差值,即便知道当时这一帧是需求核算的帧,可是没有上一帧的时刻戳,也就无法核算出开端滑动的这一帧实在的耗时。
  冻动占比

冻帧是 Google 官方界说的一种帧:

Frozen frames are UI frames that take longer than 700ms to render.

冻帧作为一种特别的帧,不是被强烈建议不要呈现的帧,在华为等文档中也被提及过。一旦呈现此类帧,页面也就像冻住似的。所以,在 APM 中,也将这一类特别的帧纳入监控规模,核算出冻帧占比:

冻帧占比 = 滑动进程中的冻帧数量 / 滑动发生的帧数

  scrollHitchRate

scrollHitchRate 概念来自于 iOS,首要是用于描述滑动进程中,hitch 时长的占比。什么叫 hitch?可以简略理解为单个帧耗时超越了烘托标准耗时的部分便是 hitch。image.png

核算公式如图所示:image.png

这儿的分子是指整个滑动进程中,hitch 的累加值,这儿的分母便是整个滑动耗时(包括 Fling)。

咱们可能会问: 那为什么不必FPS? 不是可以用 fps 来检测滑动卡顿状况么,为什么还要有一个 Hitch rate ?
这是因为  FPS 并不适用于一切的状况。比如当一个动画中有停顿时刻, FPS 就无法反应该动画的流通程度,并且并不是一切的运用都以达到 60  fps/120 fps 为方针,比如有些游戏只想以 30 fps 运转。而关于 Hitch rate 而言,咱们的方针永远是让它达到 0。

引进 scrollHitchRate 单纯为了处理高刷手机的数据纷歧致问题吗?不是的。咱们在收集到一个 scrollHitchRate 数据,还隐式的带上了滑动次数。例如,在手淘场景下,主页同学咨询过一个问题,会不会页面越往下刷,卡得越严峻?当收集到这个数据后,就可以进行回答了。

  帧率主因剖析


无论是滑动帧率,仍是冻帧,更多的仍是倾向于监控数据,假如想要在数据上剖分出当时帧率低的首要原因仍是没有方法入手的。

在之前烘托流程中,就讲到烘托流程首要分红哪几步,假如可以将烘托流程的每一步都进行监控,那么咱们就可以以为:当某一个异常帧呈现后,首要问题呈现在哪一个阶段了,可是咱们仍是期望不要像 Matrix 那样侵入体系代码。根据这个思路,咱们发现体系供给了满足咱们需求的 API:Window.OnFrameMetricsAvailableListener。Google Firebase 也同样在运用这个 API 进行帧数据监控,也不太会有后续的兼容性问题。

FrameMetrics,开发文档见 https://developer.android.com/reference/android/view/FrameMetrics

在异步回调给的 FrameMetrics 数据中,会告诉咱们每一帧每一个阶段的耗时,十分契合咱们的监控诉求。可是仍然有两个问题值得注重:

  • FrameMetrics API 是在 Android 24 上供给的,查看手淘用户数据可以发现,可以满足基本需求;
  • 一帧数据处理不及时会有丢数据的危险,但可以经过接口知晓丢掉了几帧数据。

下面咱们就详细查看下 FrameMetrics 数据中界说了哪些烘托阶段:

FrameMetrics 参数常量 含义
UNKNOWN_DELAY_DURATION 等候主线程耗时(VSync来了需求切换线程)
INPUT_HANDLING_DURATION 输入事情处理耗时
ANIMATION_DURATION 动画处理耗时
LAYOUT_MEASURE_DURATION layout & measure 耗时
DRAW_DURATION draw耗时
SYNC_DURATION sync耗时
COMMAND_ISSUE_DURATION issue耗时
SWAP_BUFFERS_DURATION 交流行列耗时
TOTAL_DURATION 总耗时

摘录自 Android 26。除上诉提及的字段此,还有几个比较不错的时刻戳字段,也可以探究出一些新奇的玩法,咱们可以一同探究下。

咱们有没有发现,跟烘托流程一模一样。在盯梢了下相关源码后,注册一个 listener,并没有太多的功能损耗,FrameMetrics 内部记载的时刻戳即便不注册也会进行收集,所以不会带来额外的功能开支。

首要咱们界说了一个需求进行剖析的帧耗时阈值,超越这个阈值就可以以为需求核算原因。咱们界说:当一帧某一个阶段耗时超越阈值一半即为主因,反之则主因不存在。

如此一来,针对某一个 Activity 就可以剖分出是主线程卡顿导致帧率低,仍是布局问题导致 layout & measure 慢,亦或是 draw 有问题,在功能优化时,直接锁定主因进行优化

  卡顿帧率
首要咱们再来回顾一下人眼的卡顿感知。原理上,高的帧率可以得到更流通、更逼真的动画,要生成平滑连贯的动画效果,帧速不能小于8FPS;每秒钟帧数越多,所显现的动画就会越流通。一般来说人眼能继续保存其印象1/24秒左右的图像,所以一般电影的帧速为24FPS。相关于游戏而言,无论帧率有多高,60帧或120帧,最终一般人能分辩到的不会超越30帧。电影尽管只要24帧每秒,但由于每两帧之间的距离均为1/24秒,所以人眼不不会感觉到显着的卡顿,游戏或许咱们界面的改写即便达到30帧每秒,但假如这一秒钟内,30帧不是均匀分配,就算是每秒60帧,其间59帧都十分流通,而有一帧延时超越1/24秒,仍然会让咱们感觉到显着的卡顿。

这便是咱们界面上大部分状况下都现已滑动的十分流通,可是偶然仍是会察觉到卡顿的原因。依照1/24秒的话,帧时刻在41.6ms,假如中间有超越41.6ms的话,咱们是可以感觉到卡顿的,假如依照1/30的话,帧时刻在33.3ms,假如某一帧的推迟时刻超越了33.3ms,那么人眼就容易察觉到这个进程,为了把这些卡顿的状况反映出来,咱们需求在遇到这些帧的时分做一些记载。可是假如咱们仅仅去记载进程中那些耗时超越33.3ms的帧,这种状况下,一方面会丢失掉时刻的要素,很难去衡量卡顿的严峻性(毕竟一段时刻内不间断的呈现卡顿,比偶然掉一帧要让人显着很多),另一方面,因为有多重缓冲区的影响,未必100%会掉帧,所以咱们仅仅取这个超越某一时刻的帧未必是准确的。

根据以上的考虑,这儿运用了一个瞬时FPS的概念用于衡量卡顿,瞬时FPS便是在滑动进程中发生的一些耗时比较小的区间中核算的值。例如用户滑动了500ms,这个进程可能会呈现几个用户核算的瞬时FPS。这个进程是怎样核算的?

  1. 滑动进程取得每一帧的时刻距离;
  2. 依照100(99.6ms,6帧的时刻)毫秒左右的时刻细化卡顿区间;
  3. 从时刻距离大于33.3毫秒的帧开端记载,作为区间起点;
  4. 完毕点是从起点开端的帧耗时相加,达到99.6ms并且后边的一帧耗时小于17毫秒(或许抵达最终一帧),否则会继续寻找完毕点;
  5. 这段时刻内在核算帧率,是这儿要寻找的卡顿帧率。

image.png

可以看到有3帧显着超出比较多。依照以前的核算方法,帧耗时:1535ms,   帧数量是:83,那么这个界面的FPS是54。咱们可以看到帧率的FPS比较高,完全看不到卡顿了,即便前面有一些比较高的耗时帧,可是被后续耗时正常的帧给均匀掉了。所以以前的核算方法现已不能反映出这些卡顿问题。

依照新的核算方法,应该是从第7帧开端核算第一个瞬时FPS区间,从这一帧开端,核算至少99.6ms的时刻,那么69+16+15,现已达到了100ms,3帧,所以FPS是30,因为低于50,所以这一次FPS会比记载,其间最大的帧耗时是69ms。
第二次从17帧开端,5帧114ms,FPS为43ms,最大帧距离是61ms。
第三次从26帧开端,98+10=108ms,可是后边帧的耗时时刻为19ms,超越16.6ms,所以仍然会加入一同核算。3帧,127ms,FPS为23。最大帧距离是98。
依照这次的核算,总共有3次卡顿FPS,分别是30,43,23,最大的帧耗时帧是98。
  卡顿仓库

假如运用主线程的  Looper Printer 来进行卡顿仓库 dump,会因为大量的字符串拼接而带来功能损耗。在 Android 10 上,Looper  中新增 Observer,可以功能无损的回调,但由于是 hide 的 API,则无法运用。最终的方法只能是不断向主线程 post  音讯,可每隔一段时刻就给主线程抛音讯又会给主线程带来压力。

是否有更好的方法呢?有的,经过 Choreographer postFrameCallback,自身就会 post 主线程音讯,运用两次回调之间的差值高于某一个阈值,就可以以为是卡顿。并且这个识别的卡顿,仍是滑动进程中的卡顿。

知道什么是卡顿,那什么时分  dump 呢?咱们运用了 watchdog 的机制 dump 出卡顿仓库,即在子线程 post 一个 dump  主线程的音讯,假如单帧耗时超越阈值就进行 dump,假如在规则时刻内完成当时帧,就取消 dump  的音讯。当咱们收集上来仓库后,咱们会将卡顿的仓库进行聚类,便于更好的决议首要矛盾、告警处理。

对帧数据运用的探究

AB 与 APM 结合运用

上文首要仍是讲解了咱们怎样核算出一个方针、怎样去排查问题,可是关于一个大盘方针而言,重之又重的当然是需求用来衡量优化效果的,那怎样去衡量优化呢?最好的手段是 AB。APM 方针数据与 AB 测验渠道打通,功能数据随 APM 试验产出。

这儿的AB渠道包括一休渠道、魔兔2渠道,一休渠道方针接入方法运用的是自界说方针,帧率仅仅作为方针之一接入,启动、页面等数据亦是其间之一。

一休是阿里集团一站式A/B试验的服务渠道,向各个事务供给了可视化的操作界面、科学的数据剖析、自动化的试验陈述等一站式的试验流程;经过科学的试验方法和实在的用户行为来验证最佳处理计划,然后驱动事务增长。

咱们在进行页面功能优化时,可以直接运用相关方针对基准桶与优化桶进行对比,直接而又显着的显现对页面功能的优化。

image.png

写在最终

关于手淘功能监控而言,帧率监控、卡顿监控仅仅功能监控其间的一小环,打磨好每一个细节也至关重要。相关数据除了与  AB  渠道搭配运用之外,现已与全链路排查数据、舆情数据、版别发布功能关口相打通,借用后台聚类、告警、自动化邮件陈述等数据手段透出,专有数据渠道进行承接。关于数据的态度,咱们不仅是要有,并且要全面而强壮。

在一轮又一轮的技能迭代下,手淘的高可用表现也不断完善与重构,期望在未来,手淘客户端高可用相关数据可以更好的助力研制各个环节,预防用户体会腐化,协助不断提升用户体会。