编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 01.崩溃捕获设计实践
  • 02.崩溃治理优化总结
  • 03.Native崩溃治理实践
  • 04.ANR监控设计实践
  • 05.CPU消耗优化实践
  • 06.卡顿监控设计实践
  • 07.卡顿治理优化实践
  • 08.网络分析与优化实践
  • 09.线程优化实践操作
  • 10.高性能图片优化方案
  • 11.OOM异常优化实践
  • 12.内存监控优化方案
  • 13.内存治理优化实践
  • 14.FPS监测设计实践
  • 15.进程优化设计实践
  • 16.App启动优化实践
  • 17.App页面UI优化实践
  • 18.App稳定性专项实践
  • 19.App瘦身优化实践
  • 20.常见代码优化实践
  • 21.移动端防抓包实践
  • 22.App磁盘沙盒实践
  • 23.Ping工具开发实践
  • 24.Gradle构建优化实践
  • 25.CodeReview实践总结

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.屏幕刷新流程是什么样的?如何完成一桢的交互工作(从发送消息——>到处理消息)?
  • 在探索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 分别经过哪些步骤

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
      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
      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
      image
  • doFrame 在什么时机回调
    • 每一次回调后,都需要对 Choreographer 进行 postFrameCallback 调用,而调用 postFrameCallback 就是在下一帧 CALLBACK_ANIMATION 类型的链表上进行添加一个节点。
    • 所以,doFrame 回调时机并不是这一帧开始计算,也不是这一帧上屏,而是 CPU 处理动画过程中的一个 callback。

5.6 View滑动帧率

  • 在最开始实现时,View 只要滑动就监控帧率,一直帧率产出到不滑动为止。根据需求,我们的帧率采集就变成了如下这样:
    • image
      image
  • 那怎么监控 View 是否有滑动呢?
    • 需要介绍一下这个 ViewTreeObserver.OnScrollChangedListener。那它的实现原理就是是什么呢?
    • 第一步:在ViewRootImpl#draw方法中,判断了 mAttachInfo 信息中 View 是否产生了滑动,如果产生滑动就分发出来。
    • 第二步:在 View 的 onScrollChanged 被调用的时候,对 mAttachInfo 信息中的 mViewScrollChanged 变量设置成 true。
    • 可以看到 ViewTreeObserver.OnScrollChangedListener 的回调是在 ViewRootImpl#draw 中,那么 Choreographer.FrameCallback 的回调先于 ViewTreeObserver.OnScrollChangedListener 的。
  • 对于滑动的帧率,就可以如下表示:
    • 每一帧都带上了是否滑动的状态,当某一帧是滑动的帧,就可以开始计数,一直累积时间到 1s,一个滑动帧率数据计算出来就出来了。
    • image
      image

5.7 手指滑动帧率

  • View 滚动并不代表着是用户操作导致,开始实现手指的滑动帧率。
    • 手指滑动帧率,首先需要能够接收到手指的 Touch 行为。使用dispatchTouchEvent此接口识别手指滑动。
    • image
      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
      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
贡献者: yangchong211
上一篇
13.内存治理优化实践
下一篇
15.进程优化设计实践