14.FPS监测设计实践
目录介绍
- 01.FPS整体概述
- 1.1 项目背景介绍
- 1.2 遇到问题说明
- 1.3 基础概念介绍
- 02.屏幕invalidate刷新
- 2.1 什么是流畅度
- 2.2 什么是FPS
- 2.3 View.invalidate()
- 2.4 ViewRootImpl分析
- 03.Choreographer
- 3.1 scheduleTraversals
- 3.2 编舞者作用
- 3.3 Choreographer源码
- 3.4 VSYNC信号
- 3.5 屏幕刷新流程
- 04.系统渲染机制
- 4.1 渲染流程梳理
- 4.2 三缓冲机制
- 05.FPS监控设计
- 5.1 FPS监控方案对比
- 5.2 fps采集思路
- 5.3 线上帧率监控
- 5.4 监测FPS时机考量
- 5.5 统计交互中帧率
- 5.6 View滑动帧率
- 5.7 手指滑动帧率
- 5.8 帧数据监控主因分析
- 06.其他设计说明
- 6.1 性能设计
01.ANR整体概述
1.1 项目背景介绍
- 流畅度优化有确定的衡量指标——fps
- FPS越大则滑动时的体验越流畅。也就是说,流畅度优化是 有指标衡量的、且指标能反映用户直接体验好坏的优化方向。
1.2 遇到问题说明
- 关于FPS概念问题记录
- 1.如何理解
FPS
,FPS
是怎么计算的。手机刷新机制60fps是怎么设计来的? - 2.屏幕刷新流程是什么样的?如何完成一桢的交互工作(从发送消息——>到处理消息)?
- 1.如何理解
- 在探索FPS实践上还遇到一些问题
- 非人为滑动数据参杂在 FPS 中,不能直接体现用户操作体验
- 计算平均数据时,卡顿数据被淹没在海量正常数据中,一次卡顿是否只影响一个 FPS 值还是一次用户操作体验?
1.3 基础概念介绍
- 了解一些常识,人眼识别多少帧
- 12fps大概类似手动快速翻动书籍的帧率,这明显是可以感知到不够顺滑的。
- 24fps使得人眼感知的是连续线性的运动。24fps是电影胶圈通常使用的帧率,这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。
- 但是低于30fps是无法顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果,当然超过60fps是没有必要的。
- 想要解决滑动流畅度问题、提升fps,需要掌握较多的技术点:
- 技术点1:View工作原理,包括三大流程
measure/layout/draw
,自定义view等 - 技术点2:屏幕刷新机制,包括
VSync
、Choreographer
、FPS
的计算 - 技术点3:系统渲染流程,包括
UIThread
与RenderThread
、CPU与GPU 分别经过哪些步骤
- 技术点1:View工作原理,包括三大流程
02.探索屏幕刷新机制
2.1 什么是流畅度
- 流畅度,是页面在滑动、渲染等过程中的体验。
Android
系统要求每一帧都要在 16ms 内绘制完成,平滑的完成一帧意味着任何特殊的帧需要执行所有的渲染代码(包括 framework 发送给 GPU 和 CPU 绘制到缓冲区的命令)都要在 16ms 内完成,保持流畅的体验。- 如果没有在期间完成渲染秒就会发生掉帧。掉帧是用户体验中一个非常核心的问题。丢弃了当前帧,并且之后不能够延续之前的帧率,这种不连续的间隔会容易会引起用户的注意,也就是我们常说的卡顿、不流畅。
- 那么是不是1s只要绘制了60帧是不是就是流畅的呢?
- 也不一定,如果发生抖动的情况,那么肯定会有其中几帧是有问题的。其中肯定会有最大绘制帧,和最小绘制帧的情况,所以平均值,最大值最小值都是我们需要知道的。
2.2 什么是FPS
- 在 Android 中,每一帧的绘制时间不要超过 16.67ms。那么,这个 16.67ms 是怎么来的呢?
- 就是由 FPS 决定的。FPS,Frame Per Second,每秒显示的帧数,也叫帧率。
- 监测 FPS 在一定程度上可以反应应用的卡顿情况
- 原理也很简单,但前提是你对屏幕刷新机制和绘制流程很熟悉。让我们先从 View.invalidate() 说起。
- FPS计算简单逻辑公式
// 帧率 帧率 = (从第一张到最后一张图片总次数) / (从第一张到最后一张的总时间) // 变成代码跟清晰 fps = (sumFrames - lastSumFrames)/ (frameCostTimes - lastFrameCostTimes)
- 演变计算的过程大概如下
- fps = 60张图片 / 1000毫秒(从第一张到第六十张照片的总时间)
2.3 View.invalidate()
- 要探究屏幕刷新机制和 View 绘制流程
- View.invalidate() 无疑是个好选择,它会发起一次绘制流程。然后开始分析invalidate()调用链路
- 在View这个类中,调用的代码链路是:
- void invalidate()
- void invalidate(boolean invalidateCache)
- void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,boolean fullInvalidate)
- 在invalidateInternal方法源码中,可以看到调用p.invalidateChild(this, damage)这个方法,本质是调用ViewGroup.invalidateChild()
- 然后看ViewGroup这个类中,调用代码的链路是:
- void invalidateChild(View child, final Rect dirty) 主要是看里面的递归逻辑
- 这里有一个递归,不停的调用父 View 的 invalidateChildInParent() 方法,直到最顶层父 View 为止。这很好理解,仅靠 View 本身是无法绘制自己的,必须依赖最顶层的父 View 才可以测量,布局,绘制整个 View 树。但是最顶层的父 View 是谁呢?
- 是 setContentView() 传入的布局文件吗?不是,它解析之后被塞进了 DecorView 中。是 DecorView 吗?也不是,它也是有父亲的。
public final void invalidateChild(View child, final Rect dirty) { final AttachInfo attachInfo = mAttachInfo; ViewParent parent = this; if (attachInfo != null) { do { View view = null; if (parent instanceof View) { view = (View) parent; } parent = parent.invalidateChildInParent(location, dirty); } while (parent != null); } }
- DecorView 的 parent 是谁呢?这就得来到 ActivityThread.handleResume() 方法中。跟踪代码分析可以发现这个 parent 是 ViewRootImpl。
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { // 1. 回调 onResume() final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); // 2. 添加 decorView 到 WindowManager wm.addView(decor, l); }
- 第二步中实际调用的是 WindowManagerImpl.addView() 方法,WindowManagerImpl 中又调用了 WindowManagerGlobal.addView() 方法。
// 参数 view 就是 DecorView public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ViewRootImpl root; // 1. 初始化 ViewRootImpl root = new ViewRootImpl(view.getContext(), display); mViews.add(view); mRoots.add(root); // 2. 重点在这 root.setView(view, params, panelParentView); }
2.4 ViewRootImpl分析
- 上面跟着 View.invalidate() 方法一路追到 ViewGroup.invalidateChild() ,其中递归调用 parent 的 invalidateChildInParent() 方法。
- 然后看ViewRootImpl.invalidateChildInParent()方法代码。
- 无论是注释 2 处的 invalidate() 还是注释 3 处的 invalidateRectOnScreen() ,最终都会调用到 scheduleTraversals() 方法。
- scheduleTraversals() 在 View 绘制流程中是个极其重要的方法。
public ViewParent invalidateChildInParent(int[] location, Rect dirty) { // 1. 线程检查 checkThread(); if (dirty == null) { // 2. 调用 scheduleTraversals() invalidate(); return null; } else if (dirty.isEmpty() && !mIsAnimating) { return null; } // 3. 调用 scheduleTraversals() invalidateRectOnScreen(dirty); return null; }
03.Choreographer
3.1 scheduleTraversals
- 从 View.invalidate() 方法开始追踪,一直跟到 ViewRootImpl.scheduleTraversals() 方法。
- 第一步:mTraversalScheduled 是个布尔值,防止重复调用,在一次 vsync 信号期间多次调用是没有意义的
- 第二步:利用 Handler 的同步屏障机制,优先处理异步消息
- 第三步:Choreographer 登场。鼎鼎大名的 编舞者 在此就出场了!
void scheduleTraversals() { // 1. 防止重复调用 if (!mTraversalScheduled) { mTraversalScheduled = true; // 2. 发送同步屏障,保证优先处理异步消息 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 3. 最终会执行 mTraversalRunnable 这个任务 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); } }
3.2 编舞者作用
- Choreographer是什么
- Android系统从4.1(API 16)开始加入 Choreographer 这个类来协调动画(animations)、输入(input)、绘制(drawing)三个UI相关的操作。
- Choreographer 中文翻译过来是”编舞者“,字面上的意思就是优雅地指挥以上三个UI操作一起跳一支舞。Choreographer 从显示子系统接收定时脉冲(例如垂直同步——VSYNC 信号),然后安排工作以渲染下一个显示帧。
- View绘制跟Choreographer关系
- ViewRootImpl 在开始绘制时会调用 Choreographer 的 postCallback 传递一个任务,Choreographer 同步完 VSYNC 信号后再执行这个任务完成绘制。
- Choreographer跟线程关系
- 每个线程都有自己的 Choreographer,其他线程也可以发布回调以在 Choreographer 上运行,但它们是运行在 Choreographer 所属的 Looper 上。
- FrameCallback是什么
- FrameCallback 是和Choreographer 交互,在下一个 frame 被渲染时触发的接口类。
- 开发者可以使用 Choreographer#postFrameCallback 设置自己的callback 与 Choreographer 交互,你设置的 callBack 会在下一个 frame 被渲染时触发。
3.3 Choreographer源码
- 在scheduleTraversals方法中,通过Choreographer对象调用postCallback发送一个任务,先看几个问题:
- mChoreographer 是在什么时候初始化的?mTraversalRunnable 是个什么鬼?mChoreographer 是如何发送任务以及任务是如何被调度执行的?
- mChoreographer是什么时候创建的?具体看ViewRootImpl构造方法
- 初始化了 mChoreographer,调用的是 Choreographer.getInstance() 方法。mChoreographer 保存在 ThreadLocal 中的线程私有对象。
public ViewRootImpl(Context context, Display display) { // 初始化 Choreographer,通过 Thread-local 存储 mChoreographer = Choreographer.getInstance(); }
- mTraversalRunnable 是个什么鬼?它是一个Runnable,主要看一下run方法中的代码
- mTraversalRunnable 被执行后最终会调用 performTraversals() 方法,来完成整个 View 的测量,布局和绘制流程。
void doTraversal() { if (mTraversalScheduled) { // 1. mTraversalScheduled 置为 false mTraversalScheduled = false; // 2. 移除同步屏障 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); // 3. 开始布局,测量,绘制流程 performTraversals(); } }
- mTraversalRunnable 是如何被调度执行的?这里重点看 mChoreographer 是如何发送任务以及任务是如何被调度执行的逻辑
- mChoreographer.postCallback(Choreographer.CALLBACK_INPUT,mConsumedBatchedInputRunnable, null);
- 然后 --> postCallbackDelayed ---> postCallbackDelayedInternal ---> 2和3最后执行的仍然是 scheduleFrameLocked(now) 方法
private void postCallbackDelayedInternal(int callbackType,Object action, Object token, long delayMillis) { synchronized (mLock) { // 1. 将 mTraversalRunnable 塞入队列 mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); if (dueTime <= now) { //2. 由于 delayMillis 是 0,所以会执行到这里 scheduleFrameLocked(now); } else { //3. 延迟执行 Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); msg.arg1 = callbackType; msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, dueTime); } } }
3.4 VSYNC信号
- 看到mChoreographer.postCallback发送消息,最终都会执行到scheduleFrameLocked(now) 方法。
private void scheduleFrameLocked(long now) { if (!mFrameScheduled) { mFrameScheduled = true; if (USE_VSYNC) { // Android 4.1 之后 USE_VSYNCUSE_VSYNC 默认为 true // 如果是当前线程,直接申请 vsync,否则通过 handler 通信 if (isRunningOnLooperThreadLocked()) { scheduleVsyncLocked(); } else { // 发送异步消息 Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); msg.setAsynchronous(true); mHandler.sendMessageAtFrontOfQueue(msg); } } else { // 未开启 vsync,4.1 之后默认开启 final long nextFrameTime = Math.max(mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now); Message msg = mHandler.obtainMessage(MSG_DO_FRAME); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, nextFrameTime); } } }
- 不管是哪一种消息,最终都会执行到scheduleVsyncLocked()方法。已经好几次提到了 VSYNC ,思考一下Vsync是什么东西?
- 第一步:在 scheduleVsync() 方法中会通过 nativeScheduleVsync() 方法注册下一次 vsync 信号的监听
- 第二步:当下次 vsync 信号来临时,会通过 jni 回调 java 层的 dispatchVsync() 方法,其中又调用了 onVsync() 方法。
- 当消息被执行时,调用的是自己的 run() 方法,run() 方法中调用的是 doFrame() 方法。
private void scheduleVsyncLocked() { //第一步:注册信号监听 mDisplayEventReceiver.scheduleVsync(); } //在DisplayEventReceiver类中 public void scheduleVsync() { // 注册监听 vsync 信号,会回调 dispatchVsync() 方法 nativeScheduleVsync(mReceiverPtr); } //在Choreographer类中 private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable { //第二步:vsync 信号监听回调 @Override public void onVsync(long timestampNanos, int builtInDisplayId, int frame) { //向主线程发送了一个异步消息 } @Override public void run() { doFrame(mTimestampNanos, mFrame); } }
- VSYNC 的作用是什么?
- 可以把 VSYNC 看成一个由硬件发出的定时信号,通过 Choreographer 监听这个信号。每当信号来临时,统一开始绘制工作。这就是 scheduleVsyncLocked() 方法的工作内容。
- VSYNC 主要是解决什么问题?
- VSYNC 是为了解决屏幕刷新率和 GPU 帧率不一致导致的 “屏幕撕裂” 问题。在 4.1 之后,Google 才将其引入到 Android 显示系统中,以解决饱受诟病的 UI 显示不流畅问题。
3.5 屏幕刷新流程
- 屏幕刷新的整个流程再次梳理一下。
- 了解这个主要是方便掌握fps整个原理。关注的核心点是:View刷新机制,Vsync信号传递和处理。
- 第一步:从 View.invalidate() 开始
- 最后会递归调用 parent.invalidateChildInParent() 方法。这里最顶层的 parent 是 ViewRootImpl 。ViewRootImpl 是 DecorView 的 parent。
- 这个赋值调用链是这样的 ActivityThread.handleResumeActivity -> WindowManagerImpl.addView() -> WindowManagerGlobal.addView() -> ViewRootImpl.setView()
- 第二步:ViewRootImpl.invalidateChildInParent() 最终调用到 scheduleTraversals() 方法
- 建立同步屏障之后,通过 Choreographer.postCallback() 方法提交了任务 mTraversalRunnable,这个任务就是负责 View 的测量,布局,绘制。
- 第三步:Choreographer发送和处理消息
- Choreographer.postCallback() 方法通过 DisplayEventReceiver.nativeScheduleVsync() 方法向系统底层注册了下一次 vsync 信号的监听。
- 当下一次 vsync 来临时,系统会回调其 dispatchVsync() 方法,最终回调 FrameDisplayEventReceiver.onVsync() 方法。
- 第四步:回调处理onVsync方法然后执行doFrame完成绘制流程
- FrameDisplayEventReceiver.onVsync() 方法中取出之前提交的 mTraversalRunnable 并执行。这样就完成了整个绘制流程。
04.系统渲染机制
4.1 渲染流程梳理
- 系统渲染机制流程图如下所示
image
- 第一个阶段:Vsync信号的传递和处理。这个是native和java之间信号的交互。
- 触发渲染后,会走到 ViewRootImpl 的 scheduleTraversals。这时,scheduleTraversals 方法主要是向 Choreographer 注册下一个 VSync 的回调。
- 第一步:在 scheduleVsync() 方法中会通过 nativeScheduleVsync() 方法注册下一次 vsync 信号的监听
- 第二步:当下次 vsync 信号来临时,会通过 jni 回调 java 层的 dispatchVsync() 方法,其中又调用了 onVsync() 方法。
- 第二个阶段:在主线程中做CPU计算逻辑。比较熟悉的就是View的测量,布局,绘制步骤。
- 当下一个 VSync 来临时,Choreographer 首先切到主线程(传 VSync 上来的 native 代码不运行在主线程),当然它并不是直接给 Looper sendMessage,而是 msg.setAsynchronous(true) ,提高了 UI 的响应速率。
- 切到主线程后,Choreographer 开始执行所有注册了这个 VSync 的回调。目前注册回调类型有:输入事件,动画处理,UI分发等……
- 第一步:Choreographer 会将所有的回调按类型分类,用链表来组织,表头存在一个大小固定的数组中(因为只支持这四种回调)。在 VSync 发送到主线程的消息中,就会一条链表一条链表的取出顺序执行并清空。
- 第二步:在 scheduleTraversals 注册的就是 CALLBACK_TRAVERSAL 类型的 callback,这个 callback 中执行的就是我们最为熟悉的 ViewRootImpl#doTraversal() 方法,doTraversal 方法中调用了 performTraversals 方法,performTraversals 方法中最重要的就是调用了耳熟能详的 performMeasure、performLayout、performDraw 方法。
- 第三个阶段:在GPU中做硬件绘制工作。
- CPU主线程完成后会把绘制的数据传递给应用层的RenderThread(开启硬件加速),RenderThread 会向 SurfaceFlinger 请求获取绘制数据的 Buffer。
- 这个过程也是通过 Binder 完成的,获取到 Buffer 后将主线程绘制的数据通过 GPU 绘制(通常是通过 OpenGL ES)到 Buffer 上
- 第四个阶段:获取缓存区buffer数据,传递给 SurfaceFlinger,最后将合成的数据传递给HAL层。
- RenderThread 通过 Binder 将填充好数据的 Buffer 传递给 SurfaceFlinger,SurfaceFlinger 最后合成这些数据再传递给 HardwareComposer (HAL 层),完成一帧画面的显示。
4.2 三缓冲机制
- Android 4.1 引入了 VSync 和三缓冲机制
- VSync 给予开始 CPU 计算的时机,以及 GPU 和 Display 交换的缓冲区的时机,这样有利于充分利用时间来处理数据和减少 jank。
image
- 图中 A、B、C 分别代表着三个缓冲区
- 可以看到 CPU、GPU、显示器都能尽快拿到 buffer,减少不必要的等待。如果显示器和 GPU 现在都使用着一个 buffer,如果下一次渲染开始了,因为还有一个 buffer 可以用于 CPU 数据的写入,所以可以马上开始下一帧数据的渲染,例如图中第一个 VSync。
- 是不是引入三缓冲机制就没有任何问题呢
- 当仔细看上图可发现,数据 A 在第三个 VSync 来临时就已经准备好,随时可以刷新到屏幕上,到真正刷到屏幕却是第四个 VSync 来临。
- 由此可知,三缓冲虽然有效利用了等待 VSync 的时间,减少了 jank,但是带来了延迟。
05.FPS监控设计
5.1 FPS监控方案对比
- 第一种市场方案:在 Touch 事件后采集1s内draw的次数统计帧率
- 这个方案的优点是性能损耗低,但是存在致命缺陷。如果页面渲染总时长不足 1s 就停止刷新,会导致数据人为偏低。
- 其次,触碰屏幕不一定会带来刷新,刷新也不一定是 Touch 事件带来的。而以上情况计算出来的都是脏数据。
- Android 在 ViewRootImpl 实现了一个Debug 的 FPS 方案,原理与上诉方案类似,都是在 draw 时累积时长到 1s,所以,如果是想要一个低成本性能无损的线下测试 FPS,这不失为一个方案。
- 第二种市场方案:Matrix hook 处理 Choreographer 统计帧率
- Matrix 创新性的 hook 了 Choreographer 的 CallbackQueue,同时还通过反射调用 addCallbackLocked 在每一个回调队列的头部添加了自定义的 FrameCallback。
- 如果回调了这个 Callback,那么这一帧的渲染也就开始了,当前在 Looper 中正在执行的消息就是渲染的消息。这样除了监控帧率外,还能监控到当前帧的各个阶段耗时数据。
- 帧率回调和 Looper 的 Printer 结合使用,能够在出现卡顿帧的时候去 dump 主线程信息,便于业务方解决卡顿,但是频繁拼接字符串会带来一定的性能开销(println 方法调用时有字符串拼接)。
- 第三种市场方案:使用 Choreographer.FrameCallback 的 doFrame(frameTimeNanos: Long) 方法统计帧率
- 使用 Choreographer.FrameCallback 的 doFrame(frameTimeNanos: Long) 方法,在每一次的回调里计算两帧之差,通过计算可以得到 FPS。
5.2 fps采集思路
- 一般常规的Fps采集可以通过Choreographer既UI线程绘制的编舞者
- Choreographer是一个ThreadLocal的单例,接收vsync信号进行界面的渲染,我们只要对其添加一个CallBack,就可以巧妙的计算出这一帧的绘制时长。
- Matrix对于核心Choreographer是对CallbackQueue的hook,通过hook addCallbackLocked分别在不同类型的回调队列的头部添加自定义的FrameCallback。
- FPS采集方案说明
- 第一种方案:采用Choreographer.FrameCallback监听doFrame回调次数。
- 第二种方案:采用addOnFrameMetricsAvailableListener,只支持7.0以上的版本。Google Firebase 也同样在使用这个 API 进行帧数据监控,也不太会有后续的兼容性问题。
5.3 线上帧率监控
- 如何实现线上用户的帧率监控
- 可以通过 Choreographer.FrameCallback 回调来实现帧率监控,具体的代码可以看 PerformanceManager。大概思路如下所示:
- 首先开启一个handler消息,执行一个异步定时任务,每1000ms执行一次,用于统计1秒内的帧率。
- 使用 Choreographer.getInstance().postFrameCallback(this) 注册 VSYNC 信号回调监听,当 VSYNC 信号返回时,会执行 doFrame 回调函数。
- 在 doFrame 方法中,我们统计每秒内的执行次数,以及记录当前帧的时间,并注册一下次监听(调用postFrameCallback方法)。
- 如何统计帧率原理
- 从源码分析Choreographer是如何实现VSYNC信号的请求及帧的刷新处理?(Android Q)
- https://blog.csdn.net/u011578734/article/details/109625782
5.4 监测FPS时机考量
- 在手机"静止"时监控FPS是没有什么意义的,因为这时应用根本就没有做任何事,这时的FPS应该恒为60FPS,相差应该不大。
- 主线程的Looper-MessageQueue只有在有消息的时候才会运行,在没有消息的时候Looper-MessageQueue实际上是"暂停"的。
- 如何监听主线程在"运行"状态,只有在主线程Looper-MessageQueue运行时才会监控应用的FPS。可以使用打印日志中"<<<<<"来判断,
public static void loop() { for (;;) { Message msg = queue.next(); // might block if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } } }
5.5 统计交互中帧率
- 计算出 FPS 并不是我们的目标
- 希望计算出的是滑动帧率,针对 FPS,我们更为关注的是用户在交互过程中的帧率,监控这一类帧率才能更好反映用户体验。
- 怎么计算出一个 FPS 值呢?
- Choreographer.FrameCallback 被回调时,doFrame 方法都带上了一个时间戳,计算与上一次回调的差值,就可以将之视之为一帧的时间。当累加超过 1s 后,就可以计算出一个 FPS 值。
image
- doFrame 在什么时机回调
- 每一次回调后,都需要对 Choreographer 进行 postFrameCallback 调用,而调用 postFrameCallback 就是在下一帧 CALLBACK_ANIMATION 类型的链表上进行添加一个节点。
- 所以,doFrame 回调时机并不是这一帧开始计算,也不是这一帧上屏,而是 CPU 处理动画过程中的一个 callback。
5.6 View滑动帧率
- 在最开始实现时,View 只要滑动就监控帧率,一直帧率产出到不滑动为止。根据需求,我们的帧率采集就变成了如下这样:
image
- 那怎么监控 View 是否有滑动呢?
- 需要介绍一下这个 ViewTreeObserver.OnScrollChangedListener。那它的实现原理就是是什么呢?
- 第一步:在ViewRootImpl#draw方法中,判断了 mAttachInfo 信息中 View 是否产生了滑动,如果产生滑动就分发出来。
- 第二步:在 View 的 onScrollChanged 被调用的时候,对 mAttachInfo 信息中的 mViewScrollChanged 变量设置成 true。
- 可以看到 ViewTreeObserver.OnScrollChangedListener 的回调是在 ViewRootImpl#draw 中,那么 Choreographer.FrameCallback 的回调先于 ViewTreeObserver.OnScrollChangedListener 的。
- 对于滑动的帧率,就可以如下表示:
- 每一帧都带上了是否滑动的状态,当某一帧是滑动的帧,就可以开始计数,一直累积时间到 1s,一个滑动帧率数据计算出来就出来了。
image
5.7 手指滑动帧率
- View 滚动并不代表着是用户操作导致,开始实现手指的滑动帧率。
- 手指滑动帧率,首先需要能够接收到手指的 Touch 行为。使用dispatchTouchEvent此接口识别手指滑动。
image
- 这个时候需要知道几个时机问题:
- 有 dispatchTouchEvent 不会立马产生 doFrame
- 通过 dispatchTouchEvent 计算移动时间/距离超过 TapTimeout/ScaledTouchSlop,不一定立马产生 doFrame
- 通过 dispatchTouchEvent 计算移动时间/距离超过 TapTimeout/ScaledTouchSlop 时,只会给一个 flag,通知后面的 ViewTreeObserver.OnScrollChangedListener 的 doFrame 可以开始计算成手指滑动帧率。
5.8 帧数据监控主因分析
- 帧率主因分析
- 如果能够将渲染流程的每一步都进行监控,那么我们就可以认为:当某一个异常帧出现后,主要问题出现在哪一个阶段。
- 系统提供了满足我们需求的 API:Window.OnFrameMetricsAvailableListener。
- 在异步回调给的 FrameMetrics 数据中,会告诉我们每一帧每一个阶段的耗时,非常契合监控诉求。
- FrameMetrics 内部记录的时间戳即使不注册也会进行采集,所以不会带来额外的性能开销。
- 定义了一个需要进行分析的帧耗时阈值,超过这个阈值就可以认为需要统计原因。我们定义:当一帧某一个阶段耗时超过阈值一半即为主因,反之则主因不存在。
- 针对某一个 Activity 就可以分析出是主线程卡顿导致帧率低,还是布局问题导致 layout & measure 慢,亦或是 draw 有问题,在性能优化时,直接锁定主因进行优化。
06.其他设计说明
6.1 性能设计
- 有哪些性能上的优化设计呢
- 滑动次数识别
- 思考一下这个问题
- 收到每一帧的 doFrame 回调后,都需要重新 postFrameCallback。每一次 postFrameCallback 都会注册 VSync(如果没有被注册),当 Vsync 来临后,会给主线程抛一个消息,这势必会给主线程带来一定的压力。
- 系统在页面静止的时候是不会进行渲染的,也就不会有 VSync 被注册。那么在没有渲染的时候,是否也需要 post 呢?不需要,没有意义,是可以过滤掉的。基于这个理念,对滑动帧率的计算进行了优化。
image
- 需要减少非必要的帧回调与注册,就需要明确几个问题,那么该如何来做性能优化呢
- 起点(什么时候开始 postFrameCallback):在第一次收到 scroll 事件的时候(onScrollChanged)
- 终点(什么时候不再 postFrameCallback):在计算完一个手指滑动 FPS 后,如果下一帧不再滑动,那么就停止注册下一帧的回调。
参考博客
- 手淘 Android 帧率采集与监控详解
- https://mp.weixin.qq.com/s?__biz=Mzg4MjE5OTI4Mw==&mid=2247494823&idx=1&sn=8d57d026a618711b4b459ac60a1597ef&chksm=cf58f33bf82f7a2dd7a9d76cd30abf6b4622632518b0ff111c4facbcd4509d99c7fa499a7681&scene=178&cur_album_id=2495132597375975425#rd