共计 9247 个字符,预计需要花费 24 分钟才能阅读完成。
作者:黎磊(千诺)
APM 提供帧率的相干数据,即 FPS(Frames Per Second) 数据。FPS 在肯定水平上反映了页面晦涩水平,但 APM 提供的 FPS 并不是很精确。恰逢手淘低端机性能优化我的项目开启,亟需相干指标来掂量对滑动体验的优化,帧率数据摸索实际就此拉开。
在摸索实际中,咱们遇到了许多问题:
- 高刷手机占比绝对不低,影响整体 FPS 数据
- 非人为滑动数据参杂在 FPS 中,不能间接体现用户操作体验
- 计算均匀数据时,卡顿数据被吞没在海量失常数据中,一次卡顿是否只影响一个 FPS 值还是一次用户操作体验?
通过一段时间的摸索,咱们积淀下来了一些指标,其中包含:滑动帧率、冻帧占比、scrollHitchRate、卡顿帧率。除了相干帧率指标之外,为了更好的领导性能优化,APM 还提供了帧率主因剖析,同时为了更好的定位卡顿问题,也提供了卡顿堆栈。
上面是 APM 基于平台的个性,对帧率相干摸索实际的具体介绍,心愿本文能够给大家带来一些帮忙。
零碎渲染机制
在介绍指标的实现之前,首先须要理解零碎是如何做渲染的,只有通晓零碎渲染机制,能力帮忙咱们更好的进行帧率数据计算解决。
渲染机制是 Android 中重要的一部分,其中又牵扯甚广,包含咱们常说的 measure/layout/draw 原理、卡顿、适度绘制等,都与其相干。在这里咱们次要是对渲染流程进行整体理解,通晓后续须要计算哪几局部、通过零碎 API 失去了哪几局部,以便计算出指标数据。
渲染流程
咱们都晓得,当触发渲染后,会走到 ViewRootImpl 的 scheduleTraversals。这时,scheduleTraversals 办法次要是向 Choreographer 注册下一个 VSync 的回调。当下一个 VSync 来长期,Choreographer 首先切到主线程(传 VSync 上来的 native 代码不运行在主线程),当然它并不是间接给 Looper sendMessage,而是 msg.setAsynchronous(true),进步了 UI 的响应速率。
当切到主线程后,Choreographer 开始执行所有注册了这个 VSync 的回调,回调类型分为以下四种:
- CALLBACK_INPUT,输出事件
- CALLBACK_ANIMATION,动画解决
- CALLBACK_TRAVERSAL,UI 散发
- 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。
上图中 A、B、C 别离代表着三个缓冲区。咱们能够看到 CPU、GPU、显示器都能尽快拿到 buffer,缩小不必要的期待。如果显示器和 GPU 当初都应用着一个 buffer,如果下一次渲染开始了,因为还有一个 buffer 能够用于 CPU 数据的写入,所以能够马上开始下一帧数据的渲染,例如图中第一个 VSync。
是不是引入三缓冲机制就没有任何问题呢,当咱们认真看上图可发现,数据 A 在第三个 VSync 来长期就曾经筹备好,随时能够刷新到屏幕上,到真正刷到屏幕却是第四个 VSync 降临。由此可知,三缓冲尽管无效利用了期待 VSync 的工夫,缩小了 jank,然而带来了提早。
这里只是简略带大家回顾了这块的常识,倡议大家翻下倒退的历史,知其然亦要知其所以然。
对帧数据信息的开掘
当咱们晓得了整个零碎渲染的流程后,咱们须要监控什么,怎么监控,这是一个问题。
业界计划
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 值呢?
Choreographer.FrameCallback 被回调时,doFrame 办法都带上了一个工夫戳,计算与上一次回调的差值,就能够将之视之为一帧的工夫。当累加超过 1s 后,就能够计算出一个 FPS 值。
在这个过程中,有个点要大家通晓,doFrame 在什么机会回调:
首先,咱们每一次回调后,都须要对 Choreographer 进行 postFrameCallback 调用,而调用 postFrameCallback 就是在下一帧 CALLBACK_ANIMATION 类型的链表上进行增加一个节点。所以,doFrame 回调机会并不是这一帧开始计算,也不是这一帧上屏,而是 CPU 解决动画过程中的一个 callback。
当计算出一个 FPS 值后,就须要在下面叠加以下状态了:
View 滑动帧率
在最开始实现时,View 只有滑动就监控帧率,始终帧率产出到不滑动为止。依据需要,咱们的帧率采集就变成了如下这样:
那怎么监控 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 的。
对于单帧,就能够如下示意:
这样,每一帧都带上了是否滑动的状态,当某一帧是滑动的帧,就能够开始计数,始终累积工夫到 1s,一个滑动帧率数据计算出来就进去了。
手指滑动帧率
View 滑动帧率,在线下验证时,与测试平台出的数据统一,并且可能合乎根本需要,验收通过。上线后,也开始了运行,并可能承当起帧率相干工作。
然而,View 滚动并不代表着是用户操作导致,数据始终不全是用户体验的后果。所以,咱们开始实现手指的滑动帧率。
手指滑动帧率,首先咱们须要可能接管到手指的 Touch 行为。因为 APM 中已有对 Callback 的 dispatchTouchEvent 接口的 hook,所以决定间接应用此接口辨认手指滑动。
这个时候,咱们须要晓得几个机会问题:
- 有 dispatchTouchEvent 不会立马产生 doFrame
- 通过 dispatchTouchEvent 计算挪动工夫 / 间隔超过 TapTimeout/ScaledTouchSlop,不肯定立马产生 doFrame
所以,通过 dispatchTouchEvent 计算挪动工夫 / 间隔超过 TapTimeout/ScaledTouchSlop 时,只会给一个 flag,告诉前面的 ViewTreeObserver.OnScrollChangedListener 的 doFrame 能够开始计算成手指滑动帧率。
性能优化 / 滑动次数辨认
咱们在收到每一帧的 doFrame 回调后,都须要从新 postFrameCallback。每一次 postFrameCallback 都会注册 VSync(如果没有被注册),当 Vsync 降临后,会给主线程抛一个音讯,这势必会给主线程带来肯定的压力。
家喻户晓,零碎在页面静止的时候是不会进行渲染的,也就不会有 VSync 被注册。那么在没有渲染的时候,是否也须要 post 呢?不须要,没有意义,是能够过滤掉的。基于这个理念,咱们对滑动帧率的计算进行了优化。
须要缩小非必要的帧回调与注册,就须要明确几个问题:
- 终点(什么时候开始 postFrameCallback):在第一次收到 scroll 事件的时候(onSrollChanged)
- 起点(什么时候不再 postFrameCallback):在计算完一个手指滑动 FPS 后,如果下一帧不再滑动,那么就进行注册下一帧的回调。
如果仔细的话,就会发现,这里的终点能够认为是手指带来的滑动的渲染终点,这里的起点能够认为是手指带来的滑动的渲染起点(包含了 Fling),这个数据很重要,咱们相当于辨认了一次手指滑动,并且可能提供每次手指滑动的耗时等数据。
这样进行优化是否就白璧无瑕呢?其实不是的,认真看上图的计算开始工夫点,就会发现:损失了开始滑动的第一帧数据。因为咱们计算的是两次 doFrame 回调的差值,即便晓得以后这一帧是须要计算的帧,然而没有上一帧的工夫戳,也就无奈计算出开始滑动的这一帧真正的耗时。
冻帧占比
冻帧是 Google 官网定义的一种帧:
Frozen frames are UI frames that take longer than 700ms to render.
冻帧作为一种非凡的帧,不是被强烈建议不要呈现的帧,在华为等文档中也被提及过。一旦呈现此类帧,页面也就像冻住似的。所以,在 APM 中,也将这一类非凡的帧纳入监控范畴,计算出冻帧占比:
冻帧占比 = 滑动过程中的冻帧数量 / 滑动产生的帧数
scrollHitchRate**
scrollHitchRate 概念来自于 iOS,次要是用于形容滑动过程中,hitch 时长的占比。什么叫 hitch?能够简略了解为单个帧耗时超过了渲染规范耗时的局部就是 hitch。
计算公式如图所示:
这里的分子是指整个滑动过程中,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…
在异步回调给的 FrameMetrics 数据中,会通知咱们每一帧每一个阶段的耗时,十分符合咱们的监控诉求。然而仍然有两个问题值得器重:
- FrameMetrics API 是在 Android 24 上提供的,查看手淘用户数据能够发现,可能满足根本需要;
- 一帧数据处理不及时会有丢数据的危险,但能够通过接口通晓抛弃了几帧数据。
上面咱们就具体查看下 FrameMetrics 数据中定义了哪些渲染阶段:
摘抄自 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。这个过程是怎么计算的?
- 滑动过程取得每一帧的工夫距离;
- 依照 100(99.6ms,6 帧的工夫)毫秒左右的工夫细化卡顿区间;
- 从工夫距离大于 33.3 毫秒的帧开始记录,作为区间终点;
- 完结点是从终点开始的帧耗时相加,达到 99.6ms 并且前面的一帧耗时小于 17 毫秒(或者达到最初一帧),否则会持续寻找完结点;
- 这段时间外在统计帧率,是这里要寻找的卡顿帧率。
能够看到有 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 试验的服务平台,向各个业务提供了可视化的操作界面、迷信的数据分析、自动化的实验报告等一站式的试验流程;通过迷信的试验办法和实在的用户行为来验证最佳解决方案,从而驱动业务增长。
咱们在进行页面性能优化时,可能间接应用相干指标对基准桶与优化桶进行比照,间接而又显著的显示对页面性能的优化。
[]()
写在最初
对于手淘性能监控而言,帧率监控、卡顿监控只是性能监控其中的一小环,打磨好每一个细节也至关重要。相干数据除了与 AB 平台搭配应用之外,曾经与全链路排查数据、舆情数据、版本公布性能关口相买通,借用后盾聚类、告警、自动化邮件报告等数据伎俩透出,专有数据平台进行承接。对于数据的态度,咱们不仅是要有,而且要全面而弱小。
在一轮又一轮的技术迭代下,手淘的高可用体现也不断完善与重构,心愿在将来,手淘客户端高可用相干数据可能更好的助力研发各个环节,预防用户体验腐化,帮忙一直晋升用户体验。
关注【阿里巴巴挪动技术】微信公众号,每周 3 篇挪动技术实际 & 干货给你思考!