Android应用卡顿监控方案原理和对比

news2024/10/7 15:23:00

作者:天才木木木木

0 介绍

要监控应用界面是否发生卡顿,需要先了解一下Android应用主线程的渲染机制:
Android 系统提供一个稳定的帧率输出机制,让软件层和硬件层可以以共同的频率一起工作,使我们可以享受稳定帧率的画面。

大部分手机的屏幕都是60Hz的刷新率,系统为了配合屏幕的刷新频率,每过16.6ms就会发出Vsync信号来通知应用进行绘制。如果每个Vsync周期应用都能完成渲染逻辑,那么应用的FPS就是60,给用户的感觉就是非常流畅。

在应用层,实现上述机制的关键类就是Choreographer。每隔16.6ms,Vsync 信号唤醒 Choreographer来做应用的绘制操作。

想要监控卡顿或者是监测App的流畅度,就必须通过代码手段来获取FPS或者每帧耗时,并转化成可以衡量应用卡顿程度的指标。而几乎所有的卡顿监控方案都离不开Choreographer这个类。所以先简单说说Choreographer

Choreographer

在应用层就是通过Choreographer来接受VSync信号并执行每一帧的渲染逻辑。
每当Vsync到来时,会往主线程消息队列里添加一个Message,最终其doFrame函数将被调用:

//Choreographer.java
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
    ......
    mTimestampNanos = timestampNanos;
    mFrame = frame;
    Message msg = Message.obtain(mHandler, this);
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

public void run() {
    ......
    doFrame(mTimestampNanos, mFrame);
}

doFrame函数中执行了应用层的callback,基本上包含了一帧的渲染工作:

//Choreographer.java
void doFrame(long frameTimeNanos, int frame) {
    //自带了掉帧计算
    if (jitterNanos >= mFrameIntervalNanos) {
        final long skippedFrames = jitterNanos / mFrameIntervalNanos;
        if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
            Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                    + "The application may be doing too much work on its main thread.");
        }
    }
    ......
    doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
    doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
    doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
    doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
    doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    ......
}

其中CALLBACK_ANIMATION是处理动画相关的逻辑,而CALLBACK_TRAVERSAL则会调用到ViewRootImpl的performTraversals() 函数,从而执行到我们所熟悉的View的measure、layout、draw三大流程。

所以可以说,doFrame() 函数包含了应用层绘制一帧的逻辑处理。

由于Choreographer处于如此重要的一个位置,基本上所有的卡顿监控都会围绕着Choreographer进行的,除了自带的掉帧计算,Choreographer 提供的 FrameCallback 和 FrameInfo都是应用层可以直接访问的接口。

下面介绍一下市面上开源方案的几种实现方式和简单对比。

1 Choreographer的FrameCallback

TinyDancer 就是通过这种方式计算出FPS。 核心代码:

Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {
    override fun doFrame(frameTimeNanos: Long) {
        if (lastFrameTimeNanos > 0) {
            val frameTime = (frameTimeNanos - lastFrameTimeNanos) / NANOS_PER_MS
        }
        lastFrameTimeNanos = frameTimeNanos
        Choreographer.getInstance().postFrameCallback(this)
    }

})

通过记录doFrame回调的间隔时间作为每帧耗时frameTime,如此也很容易计算出FPS=1000/frameTime,掉帧数=frameTime/16.6。

2 Choreographer + Looper

Tencent/matrix 虽然也是基于Choreographer,但其监测FPS的机制和上面的FrameCallback方案不太一样。 由前面Choreographer的介绍可知,所谓每一帧其实指的就是input、animation、traversal三种事件对应的三个doCallback方法的执行结果,而matrix统计帧耗时就是通过监测这三个方法的执行总时间来表示。matrix监测FPS的主要实现在LooperMonitorUIThreadMoniter两个类里。

  1. LooperMonitor为主线程Looper设置一个Printer来监听UI线程每个Message的开始、结束,从而得到Message的执行耗时。(方案同 BlockCanary
class LooperPrinter implements Printer {
    @Override
    public void println(String x) {
        ......
        dispatch(x.charAt(0) == '>', x);
    }
    
    private void dispatch(boolean isBegin, String log) {
        for (LooperDispatchListener listener : listeners) {
            if (isBegin) {
                listener.onDispatchStart(log);
            } else {
                listener.onDispatchEnd(log);
            }
         }
    }
}
  1. UIThreadMoniter通过java反射向Choreographer的CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL三个事件的callback队列的头部插入自定义callback。
```java
    //UIThreadMonitor.java
    @Override
    public void run() {
        doFrameBegin(token);
        doQueueBegin(CALLBACK_INPUT);

        addFrameCallback(CALLBACK_ANIMATION, new Runnable() {
            @Override
            public void run() {
                doQueueEnd(CALLBACK_INPUT);
                doQueueBegin(CALLBACK_ANIMATION);
            }
        }, true);

        addFrameCallback(CALLBACK_TRAVERSAL, new Runnable() {
            @Override
            public void run() {
                doQueueEnd(CALLBACK_ANIMATION);
                doQueueBegin(CALLBACK_TRAVERSAL);
            }
        }, true);
    }

前面提到Choreographer收到VSync信号时,也是往主线程消息队列里放入一个Message最终触发doFrame。而LooperMonitor监控了每个Message执行的开始/结束,如果UIThreadMonitor的doFrameBegin被执行,则说明当前在 Looper 中正在执行的消息就是渲染的消息。然后再在Message结束的时候作为当前帧绘制的结束。这个整个一帧的监控就闭环了。
所以在Matrix中,完整的一帧耗时是onDispatchStart -> doFrame -> onDispatchEnd

因为UIThreadMonitor是在Choreographer的callback队列的头部添加callback,所以可用于记录每种类型callback队列执行的开始时间,在队列里的callback都执行完毕后,就可以计算对应的事件的耗时(CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL),比如CALLBACK_INPUT耗时 = CALLBACK_ANIMATION开始时间 - CALLBACK_INPUT开始时间

所以Matrix不仅可以得到每一帧的耗时,还能进一步得到每一帧内触摸事件、动画、View渲染三种事件的耗时情况。

3 官方方案——JankStats

JankStats —— 是官方推出的Jetpack套件中的一个新库,最早发布于2022年2月,该库提供了在运行时获取界面的每帧性能的回调,可以让开发者监测到性能问题及其发生的原因。
项目中依赖:

dependencies {
    implementation "androidx.metrics:metrics-performance:1.0.0-alpha03"
}

简单使用:

val listener = JankStats.OnFrameListener { frameData ->
    // A real app could do something more interesting, like writing the info to local storage and later on report it.
    Log.v("JankStatsSample", frameData.toString())
}
jankStats = JankStats.createAndTrack(window, listener)
jankStats.isTrackingEnabled = true

简单几行代码就可以在OnFrameListener回调中获取每一帧的性能数据FrameData, 同时JankStats库还提供了PerformanceMetricsState供开发者记录当前的界面状态,并通过FrameData回调出来,有助于了解用户在那一帧期间做了什么交互。

open class FrameData(
    frameStartNanos: Long,//当前帧开始时间
    frameDurationUiNanos: Long,//当前帧耗时
    isJank: Boolean,//是否发生了卡顿
    val states: List<StateInfo>//业务代码记录的UI状态,可以通过PerformanceMetricsState在关键业务代码处埋点
) 

原理

JankStats是一个典型的Android X库:在不同的Android版本和不同的设备上,实现行为一致的框架。

    class JankStats private constructor(window: Window, private val frameListener: OnFrameListener) {
        init {
            val decorView: View? = window.peekDecorView()
            implementation =
                when {
                    Build.VERSION.SDK_INT >= 31 -> {
                        JankStatsApi31Impl(this, decorView, window)
                    }
                    Build.VERSION.SDK_INT >= 26 -> {
                        JankStatsApi26Impl(this, decorView, window)
                    }
                    Build.VERSION.SDK_INT >= 24 -> {
                        JankStatsApi24Impl(this, decorView, window)
                    }
                    Build.VERSION.SDK_INT >= 22 -> {
                        JankStatsApi22Impl(this, decorView)
                    }
                    Build.VERSION.SDK_INT >= 16 -> {
                        JankStatsApi16Impl(this, decorView)
                    }
                    else -> {
                        JankStatsBaseImpl(this)
                    }
                }
        }
    }

阅读源码可以发现监测机制在API 24(7.0)前后有差异:
Android7.0及其以上系统,直接通过 Window 的新方法addOnFrameMetricsAvailableListener,监听回调每一帧的详细数据FrameMetrics:

internal open class JankStatsApi24Impl() {
    @RequiresApi(24)
    private fun Window.getOrCreateFrameMetricsListenerDelegator():
        DelegatingFrameMetricsListener {
        .....
            val delegates = mutableListOf<Window.OnFrameMetricsAvailableListener>()
            delegator = DelegatingFrameMetricsListener(delegates)
            //Window的这个方法可以监听每一帧的详细数据
            addOnFrameMetricsAvailableListener(delegator, frameMetricsHandler)
        }
        return delegator
    }
}

Android7.0以下设备,还是需要基于Choreographer,通过反射 Choreographer 的 mLastFrameTimeNanos 来获取当前帧的起始时间,然后通过 ViewTreeObserver的 OnPreDrawListener来感知绘制的开始,并往主线程的消息队列头部插入runnable来获取当前帧的UI线程绘制任务结束时间。

//JankStatsApi16Impl.kt
internal open class DelegatingOnPreDrawListener() : ViewTreeObserver.OnPreDrawListener {
    override fun onPreDraw(): Boolean {
        val frameStart = getFrameStartTime()
        with(decorView) {
            //往主线程的消息队列头部插入runnable
            handler.sendMessageAtFrontOfQueue(Message.obtain(handler) {
                val now = System.nanoTime()
                val frameTime = now - frameStart//获得每帧绘制的真实耗时
            })
        }
        return true
    }
    
    //反射 Choreographer 的 mLastFrameTimeNanos
    private fun getFrameStartTime(): Long {
        return choreographerLastFrameTimeField.get(choreographer) as Long
    }
}

获取到了当前帧的起始时间和绘制的Message结束时间,则可以计算出每帧绘制的真实耗时frameTime = now - frameStart

方案对比

  1. Choreographer的FrameCallback
    • 优点:
      • 简单可靠,维护成本低,使用稳定性高的系统开放API
    • 缺点:
      • 调用postFrameCallback()会不断地请求Vsync(scheduleVsyncLocked()),当界面静止时,UI线程也会不断请求VSync信号。主线程不干活的时候也会监测,可能会把有卡顿的场景的数据给稀释掉了。
      • 获取到的帧耗时并不是真实的每帧绘制耗时,而获取到≥16ms的两次doFrame的时间间隔。
    • 适用场景:
      • 比较粗粒度的掉帧数、卡顿监控。
  2. Matrix(Choreographer + Looper)
    • 优点:
      • 可以对UI线程绘制的不同阶段耗时进行较细致的监控
      • 不需要postFrameCallback,避免不断请求VSync信号
    • 缺点:
      • 维护成本高,对系统api侵入式较强,大量地通过反射来hook Choreographer和Looper,容易出现兼容问题,高版本系统可能会失效
      • 只统计了Choreographer的callback队列执行的耗时,即UI操作相关的耗时,没有包含两个VSYNC之间产生的其它非UI操作相关的message的耗时,因此统计出来的帧耗时可能偏低。
    • 适用场景:
      • 比较精细化的卡顿监控
  3. JankStats
    • 优点:
      • 官方方案,兼容性好,使用简单,可靠稳定。是在系统源码里进行埋点监测并收集数据。
      • 数据详尽,FrameMetrics提供了第三方框架难以采集的详细数据,包含总耗时,input、layout&measure、draw甚至是 sync bitmap到GPU的耗时等等(据说和adb shell dumpsys gfxinfoGPU呈现模式分析的数据一致)
      • 使用便捷,内置规则判断当前帧是否卡顿帧,并可以记录当前帧的应用状态。
      • 界面停止绘制时,不再生产帧率数据,避免脏数据。
    • 缺点:
      • 不支持个性化的定制需求。
      • 库还只是alpha版本,不是成熟的release版本,如果有坑则只能等官方更新。
      • 集成到老项目时不友好,可能需要升级AndroidX核心库、kotlin插件甚至gradle插件版本,代价大。
    • 适用场景:
      • 新项目,需要快速实现卡顿监控

结论

上面三种方案都可以获得当前应用的FPS,要基于项目情况去考虑方案选型。大型项目一般更倾向于维护成本低、稳定性高的基于系统开放API的方案,所以Choreographer的FrameCallback应该是使用最广泛的方案了。
采集到FPS只是应用卡顿监控的第一步,还需要制定科学的卡顿率指标。因为FPS并不能直观的反映应用的卡顿情况,还需要考虑“视觉惯性”,比如电影帧率仅24FPS也不觉得卡顿,但是应用如果一会30FPS一会60FPS,视觉上就会觉得很不流畅。

为了帮助到大家更好的全面清晰的掌握好性能优化,准备了相关的学习路线以及核心笔记(还该底层逻辑):https://qr18.cn/FVlo89

性能优化核心笔记:https://qr18.cn/FVlo89

启动优化

内存优化

UI优化

网络优化

Bitmap优化与图片压缩优化https://qr18.cn/FVlo89

多线程并发优化与数据传输效率优化

体积包优化

《Android 性能监控框架》:https://qr18.cn/FVlo89

《Android Framework学习手册》:https://qr18.cn/AQpN4J

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/704115.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

多元分类预测 | Matlab鲸鱼算法(WOA)优化混合核极限学习机(HKELM)分类预测,多特征输入模型,WOA-HKELM分类预测

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 多元分类预测 | Matlab鲸鱼算法(WOA)优化混合核极限学习机(HKELM)分类预测,多特征输入模型,WOA-HKELM分类预测 多特征输入单输出的二分类及多分类模型。程序内注释详细,直接替换数据就可以用。程序语言为matlab…

超级实用!详解Node.js中的mongodb模块和socket.io模块

文章目录 9. mongodb 模块安装和引入连接数据库操作集合获取集合插入文档查询文档更新文档删除文档 关闭连接 10. socket.io 模块安装和引入监听连接事件监听自定义事件在服务器端监听事件在客户端触发事件 广播消息在服务器端广播消息在客户端监听广播消息 断开连接完整示例 9…

WPF 零基础入门笔记(3):数据绑定详解

文章目录 文章合集数据绑定数据绑定实战事件通知型数据驱动&#xff0c;双向绑定资源绑定数据源绑定全局数据源后端和前端绑定问题继承事件通知&#xff0c;刷新数据事件通知强制刷新&#xff08;无效&#xff09;结论&#xff1a; 文章合集 WPF基础知识博客专栏 WPF微软文档 …

365天深度学习训练营-第T4周:猴痘病识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 我的环境&#xff1a; 语言环境&#xff1a;Python3.10.7编译器&#xff1a;VScode深度学习环境&#xff1a;TensorFlow2 一、前期工作&#xff1a; 1、导入…

小白开酒吧前要知道的几个知识(一)

第一、团队建设如果决定开酒吧&#xff0c;除非你的资金充裕&#xff0c;否则都该寻找合伙人。共同的目标会让你和合伙人之间合作的更愉快&#xff0c;所以合伙人应该选择善于沟通交流的。选定靠谱的合伙人之后&#xff0c;应该建立一个大致的团队体系&#xff0c;在团队内做好…

CAD怎么转成清晰的图片?分享几种好用的转换方法

CAD文件通常需要特定软件才能打开和编辑&#xff0c;而将其转换为图片格式可以使其更加普遍地被浏览和共享。此外&#xff0c;由于图片通常具有较小的文件大小&#xff0c;因此转换为图片格式可以更快地上传和下载&#xff0c;可以方便地用于演示和分享。那么我们怎么把CAD文件…

Python进行单元测试是怎么做的?

前言 在我的日常工作中&#xff0c;我是一名专业程序员。我使用c、c#和Javascript。我是一个开发团队的一员&#xff0c;他们使用单元测试来验证我们的代码是否按照它应该的方式工作。 在本文中&#xff0c;我将通过讨论以下主题来研究如何使用Python创建单元测试。 单元测试…

一文详解Mac搭建Vulkan开发环境

本文为Vukan系列的第二篇文章&#xff0c;上一篇文章对Vulkan进行了简单介绍&#xff0c;并对其与OpenGL的优劣势进行了比较&#xff0c;为应用开发人员在选择图形API方面提供了建议。下边开始本文的主要内容&#xff0c;详细介绍在Mac操作系统中&#xff0c;如何搭建Vulkan开发…

力扣 98. 验证二叉搜索树

题目来源&#xff1a;https://leetcode.cn/problems/validate-binary-search-tree/description/ C题解1&#xff1a;中序遍历&#xff0c;递归法。获取数组&#xff0c;如果是递增则返回true&#xff0c;否则返回false。 class Solution { public:void zhongxu(TreeNode* node…

用vue3+elementplus做的一个滚动菜单栏的组件

目录 起因概览设计及解决思路1.滚动条竖起来2.绑定菜单3.吸附优化 组件全部代码 起因 在elementplus中看到了滚动条绑定了slider&#xff0c;但是这个感觉很不实用&#xff0c;在底部&#xff0c;而且横向滚动&#xff0c;最常见的应该是那种固定在左上角的带着菜单的滚动条&a…

MySQL总结(索引,Explain,MVCC和隔离级别,锁)

文章目录 一、索引1.索引构成2.如何查找3.最左匹配原则4.覆盖索引5.减少冗余索引和重复索引1.冗余索引2.重复索引 6.索引适用情况和注意事项1.适用情况2.注意事项 二、Explain执行计划1.Explain语句 三、隔离级别与MVCC1.事前准备2.四个事务并发的问题1.脏写2.脏读3.不可重复读…

(有10个维度为(256,128,130)的tensor,计算10个tensor两两之间的相似性以及差异性,并将相似性和差异性可视化。

有10个维度为&#xff08;256,128,130&#xff09;的tensor&#xff0c;计算10个tensor两两之间的相似性以及差异性&#xff0c;并将相似性和差异性可视化。 问题描述解决办法 问题描述 有10个维度为&#xff08;256,128,130&#xff09;的tensor&#xff0c;计算10个tensor两…

Java基础复习第二天

目录 一、字符串 二、不可变的好处 三、String&#xff0c; StringBuffer and StringBuilder的区别 四、字符串池 五、新字符串&#xff08;“abc”&#xff09; 一、字符串 String 被声明为 final&#xff0c;因此它不可被继承。&#xff08;Integer 等包装类也不能被继承…

前端-基础选择器

从今天开始学习下前端的知识-查漏补缺&#xff0c;仅为自己学习记录使用 基础选择器 标签选择器类选择器id 选择器通配符选择器 标签选择器 标签名 {属性名&#xff1a;属性值; }<style>p {color: red;} </style><p>你好&#xff0c;世界</p>类选择…

Unity URP 获取Camera Stack

URP 获取Camera Stack 1.using UnityEngine.Rendering.Universal; 2.Camera.main.GetUniversalAdditionalCameraData().cameraStack

【日志加载 log4j】

日志 笔记记录 1. 日志介绍2. 日志体系结构3.Log4j开发流程4.Log4j组成4.1 Loggers 记录器4.2 Appenders 输出源4.3 Layouts 布局5. 配置文件 log4j.properties 1. 日志介绍 2. 日志体系结构 3.Log4j开发流程 1.引入依赖<dependency><groupId>log4j</groupId>…

爬虫正常用哪种代理比较好?

在进行网络爬虫时&#xff0c;使用代理可以带来许多好处&#xff0c;包括提高请求的可靠性、防止IP封锁、实现匿名浏览等。以下是一些常见的代理类型&#xff0c;你可以根据需要选择最适合的&#xff1a; 免费代理&#xff1a;免费代理可能数量众多&#xff0c;但质量和稳定性参…

Python获取某品牌加盟数据采集实现可视化数据分析

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 开发环境 : python 3.8 运行代码 pycharm 2022.3 辅助敲代码 jupyter 数据分析使用软件 模块使用 &#xff1a; requests 数据请求模块 需要安装 parsel 数据解析模块 csv pandas pyecharts 第三方模块安装&#xf…

基于Java开发的企业人力资源管理系统(ehr系统)

一、项目介绍 一款全源码可二开&#xff0c;可基于云部署、私有部署的企业级数字化人力资源管理系统&#xff0c;涵盖了招聘、人事、考勤、绩效、社保、酬薪六大模块&#xff0c;解决了从人事招聘到酬薪计算的全周期人力资源管理&#xff0c;符合当下大中小型企业组织架构管理…

BOLD信号的生理建模及其对有效连接的影响

导读 本文提供了BOLD信号生理过程的总体概述(即生成生物物理模型)&#xff0c;包括它们在生理信息动态因果模型(P-DCM)框架下的时间过程特征。BOLD信号主要由顺磁性脱氧血红蛋白的变化决定&#xff0c;而顺磁性脱氧血红蛋白的变化是氧代谢、脑血流量和脑血容量变化共同作用的结…