卡顿监测 · 方案篇 · Android卡顿监测指导原则

本文涉及的产品
应用实时监控服务ARMS - 应用监控,每月50GB免费额度
简介: 卡顿监测 · 方案篇 · Android卡顿监测指导原则

image.png

友情链接:  BaguTree《Android 面试、卡顿、ANR》分享

Tips:  关注微信公众号 小木箱成长营,回复 "卡顿监测" 可获得卡顿监测免费思维导图

一、引言

Hello,我是小木箱,欢迎来到小木箱成长营系列教程,今天将分享卡顿监测 · 方案篇 · Android卡顿监测指导原则。小木箱从七个维度将Android卡顿监测技术方案解释清楚。

第一个维度是卡顿定义,第二个维度是卡顿原因,第三个维度是业界方案,第四个维度是相关预研,第五个维度是分析工具,第六个维度是卡顿指标,第七个维度是监测SOP。

其中,卡顿原因主要是通过绘制机制的历史演进过程,分析了卡顿本质原因。

其中,业界方案主要是通过ArgusAPM、BlockCanary、QQ空间卡慢组件、Matrix和微信广研分析大厂是如何做卡顿监测的。

其中,相关预研主要是讲解了主线程Printer监测、Choreographer帧率测量和字节码插桩方案。

其中,分析工具主要是讲解了工具对比和使用指南。

其中,卡顿指标主要是教大家如何定义监测指标。

其中,监测SOP主要是以监测范围、上报时机、业务降级、注意事项和方案优化五个维度讲解整个监测流程。

image.png作者:小木箱

如果学完小木箱卡顿监测的方案篇,那么任何人做Android卡顿监测都可以拿到结果。

二、 卡顿定义

什么是卡顿?

Android5.0及以上系统中,如果主线程 + 渲染线程每一帧的执行都超过 16.6ms(60fps 的情况下),那么就可能会出现掉帧,这就是我们俗称的卡顿。

image.png

什么是卡死?

如果界面线程被阻塞超过几秒钟时间,那么用户可能会看到ANR对话框,这就是我们俗称的卡死。

image.png

卡顿原因

APP为什么滑动卡顿、不流畅? 什么情况下应用会卡?

如果想要回答APP为什么滑动卡顿、不流畅? 什么情况下应用会卡? 那么需要先简单了解安卓的屏幕刷新机制:

绘制机制历史演进

Android的绘制机制大概经历了以下四个阶段:

image.png

混沌时代(3.0前): 软件绘制

第一个阶段是混沌时代,即Android版本小于3.0版本,绘制库底层实现是基于Skia图形库,所有绘制在主线程,并不区分绘制和渲染。

如果ViewGroup控件有子View, 那么invalidate从根ViewGroup到子View全部要重绘,会造成不必要的渲染。

执行绘制和渲染是在主线程进行的,首先,调用View的Draw方法, 然后Canvas通过Skia图形库把Graphic Buffer数据通过View进行逐级派发,从而影响整个Canvas的Graphic Buffer数据,最后,SurfaceFlinger对Graphic Buffer数据进行加工合成,最终显示在DisPlayer中。

image.png

洪荒时代(3.0~4.1): 硬件加速 + DisplayList

第二个阶段是洪荒时代,即Android版本在3.0~4.1版本之间,硬件加速目前主流机型是开启的。开启硬件加速之后,所有绘制在主线程和渲染线程,硬件加速绘制库底层实现是基于openGLRender/Vulkan接口对GPU进行封装的跨平台库。

什么是硬件加速?

硬件加速,指的就是 GPU 加速,这里可以理解为用渲染线程调用 GPU 来进行渲染加速 。

硬件加速在目前的 Android 中是默认开启的, 所以如果我们什么都不设置,那么我们的进程默认都会有主线程和渲染线程(有可见的内容)。

在硬件加速过程,和软件绘制不同的是View.Draw没有真正干活,Canvas录制所有绘制命令,如果硬件加速的Window设置View为软件绘制,硬件加速便降级为软件绘制。

我们如果在 App 的 AndroidManifest 里面,在 Application 标签里面加一个

android:hardwareAccelerated="false"

我们就可以关闭硬件加速,系统检测到App关闭了硬件加速,就不会初始化RenderThread ,直接 cpu 调用 libSkia 来进行渲染:

image.png

图片来源: 高爷: Android Systrace 基础知识 - MainThread 和 RenderThread 解读

渲染过程是阻塞的,当View.Draw完成遍历,进入一个渲染信息同步的过程,会把主线程记录的绘制信息同步到渲染线程。

当绘制信息同步完毕,主线程会重新唤醒,根据记录的绘制命令,调用openGLRender/Vulkan接口与GPU通信,绘制命令同步给GPU,GPU根据绘制命令生成Graphic Buffer数据,Graphic Buffer数据交给SurfaceFlinger合成,最终显示在DisPlayer中。

如果ViewGroup控件有子View, 调用invalidate方法,当前的View才会被重绘,解决了3.0以前系统不必要的渲染问题。

image.png

什么是RenderNode?

创建视图会创建RenderNode,硬件加速中用RenderNode标识对应视图。

image.png

什么是DisplayList?

Android 使用 DisplayList 进行绘制而非直接使用 CPU 绘制每一帧。DisplayList 是一系列绘制操作的记录,抽象为 RenderNode 类。

RenderNode调用Canvas时,申请一个DisplayListCanvas并把具体的操作缓存到View的DrawOp树中, 接着将View缓存中的DrawOp树同步到RenderNode中,最后,遍历所有View进行绘制,当前根视图树绘制操作叫DisplayList。

image.png

为什么要用到DisplayList?而不是CPU直接操作?

  1. DisplayList 可以按需多次绘制而无须同业务逻辑交互
  2. 特定的绘制操作(如 translation, scale 等)可以作用于整个 DisplayList 而无须重新分发绘制操作
  3. 当知晓了所有绘制操作后,可以针对其进行优化:例如,所有的文本可以一起进行绘制一次
  4. 可以将对 DisplayList 的处理转移至另一个线程(也就是 RenderThread)
  5. 主线程在 sync 结束后可以处理其他的 Message,而不用等待 RenderThread 结束
mAttachInfo.mThreadedRenderer.draw(mView,mAttachInfo,this);
void draw(View view,AttachInfo attachInfo,DrawCallbacks callbacks) {
    final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
    choreographer.mFrameInfo.markDrawStart();
   // 更新到DisplayList里面
    updateRootDisplayList(view,callbacks);
    if (attachInfo.mPendingAnimatingRenderNodes != null) {
        final int count = attachInfo.mPendingAnimatingRenderNodes.size();
        for (int i = 0; i < count; i++) {
            registerAnimatingRenderNode(
                    attachInfo.mPendingAnimatingRenderNodes.get(i));
        }
        attachInfo.mPendingAnimatingRenderNodes.clear();
        attachInfo.mPendingAnimatingRenderNodes = null;
    }
    int syncResult = syncAndDrawFrame(choreographer.mFrameInfo);
    if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
        setEnabled(false);
        attachInfo.mViewRootImpl.mSurface.release();
        attachInfo.mViewRootImpl.invalidate();
    }
    if ((syncResult & SYNC_REDRAW_REQUESTED) != 0) {
        attachInfo.mViewRootImpl.invalidate();
    }
}

DisplayList绘制流程是怎么样的?

DisplayList绘制操作大概分为以下6步:

  1. 当调用父ViewGroup的子视图的invalidate方法进行绘制
  2. 给没有绘制的子视图的DisplayList标记一个dirty
  3. 调用子视图的父ViewGroup的invalidate方法进行绘制
  4. 调用视图的根节点ViewRoot的invalidate方法进行根视图绘制
  5. 调用rebuild方法重构根视图树DisplayList
  6. 调用rebuild方法重构根视图树DisplayList所有子视图树DisplayList

那么硬件加速和软件绘制有什么区别呢?

image.png

硬件加速和软件绘制可以通过BuildLayer进行配置化。

switch (mLayerType) {
            case LAYER_TYPE_HARDWARE:
                updateDisplayListIfDirty();
                if (attachInfo.mThreadedRenderer != null && mRenderNode.isValid()) {
                    attachInfo.mThreadedRenderer.buildLayer(mRenderNode);
                }
                break;
            case LAYER_TYPE_SOFTWARE:
                buildDrawingCache(true);
                break;
        }

如果支持硬件加速度,在绘制子view时候, 子view会hook硬件加速度方法, 直接执行updateDisplayListIfDirty。

如果支持软件绘制,那么首先,通过BuildCache来创建bitmap。然后,将bitmap绘制在硬件录音机Canvas上。最后,Canvas执行onDraw和dispatchDraw。

image.png

上古时代(4.1~5.0): Vsync + 三缓冲区

第三个阶段是上古时代,即Android版本在4.1~5.0版本之间。

什么是屏幕刷新频率?

一秒内显示了多少帧的图像,单位 Hz。

image.png

什么是帧率?

GPU 在一秒内绘制操作的帧数,单位 fps,Android 系统则采用更加流畅的60FPS,即每秒钟GPU最多绘制 60 帧画面。

帧率是动态变化的,例如当画面静止时,GPU 是没有绘制操作的,屏幕刷新的还是Buffer中的数据,即GPU最后操作的帧数据。

FPS 可以衡量一个界面的流畅性,但往往不能很直观的衡量卡顿的发生。一个稳定在 40、50 FPS 的页面,我们不会认为是卡顿的,但一旦 FPS 很不稳定,人眼往往很容易感知到,因此我们可以通过掉帧程度来衡量卡顿。 业界都是使用 Choreographer 来监听应用的帧率,跟卡顿不同的是,需要排除掉页面没有操作的情况,我们应该只在界面存在绘制的时候做统计。

image.png

如何监听界面是否存在绘制行为呢?

如何监听界面是否存在绘制行为呢?可以通过 addOnDrawListener 实现

getWindow().getDecorView().getViewTreeObserver().addOnDrawListener()

什么是画面撕裂问题?

一个屏幕内的数据来自2个不同的帧,画面会出现撕裂感。

image.png

怎么解决画面撕裂问题?

为了解决Android系统画面撕裂问题,即一个屏幕内的Back Buffer数据来自2个不同的帧,画面错位。

引入了双缓概念, 当屏幕刷新时,Back Buffer并不会发生变化,后台Back Buffer准备就绪后,Back Buffer和Frame Buffer才进行交换。

什么是双缓冲?

所谓的双缓存就是CPU/GPU写数据到Back Buffer,DisPlayer从Frame Buffer取数据。


image.png

Back Buffer和Frame Buffer的交换时机是什么时候呢?

Vsync是Back Buffer数据和Frame Buffer数据进行交换的最佳时间点。

image.png

如何理解Vsync+双缓存

Android4.1以前使用的是双缓存+Vsync , 怎么理解双缓存+Vsync呢?

双缓存+Vsync是指双缓存交换最佳时间点是在Vsyn,在Frame Buffer和Back Buffer交换后,屏幕获取Frame buffer的新数据,而Back Buffer可以为GPU准备下一帧Back Buffer数据。

Google在Android 4.1系统中,如何触发Vsync时机窗口呢?

在Android 4.1系统中,系统每隔16.6ms会触发一次Vsync通知,CPU和GPU收到Vsync通知后,CPU和GPU立刻开始计算,然后把数据写入Back Buffer,一定程度上避免了丢帧。

Vsync + 双缓冲区为什么解决不了丢帧问题?

但执行Back Buffer数据和Frame Buffer数据交换过程中,是有时间开销的,下一次执行Back Buffer数据和Frame Buffer数据交换时间依然超过16.6ms的,所以依然会出现丢帧情况。

Vsync + 三缓冲区是如何解决丢帧问题的?

Android4.1引入了三缓存,即Back Buffer和Frame Buffer双缓冲机制基础上增加了Graphic Buffer缓冲区。

image.png

虽然Graphic Buffer 占用了内存, 而且相比双缓冲区有所延迟,但是Back Buffer、Frame Buffer和Graphic Buffer三缓冲有效利用了等待Vsync的时间,减少了丢帧。以上, 就是Android屏幕刷新原理

image.png

末法时代(5.0后): RenderThread

第三个阶段是末法时代,即Android版本在5.0以后的版本。

因为View自身创建、绘制复杂和主线程被阻塞,无法及时绘制等原因,而16.6ms内需要完成UI创建、绘制、渲染、上屏。

影响CPU的大头是UIThread, 分别对应着View的Create、Measure、Layout和Draw ,为此在16.6ms内,提升View的绘制效率是解决卡顿问题的关键。

image.png

非UIThread的执行逻辑导致的卡顿需要根据具体业务场景分析,比如影视播放卡顿可能是播放器原因,可能是网络原因等等。UIThread和RenderThread里面的卡顿有如下几类的原因:

RenderThread阻塞

什么是RenderThread?

image.png

所谓的有可见内容的时候进行渲染的线程,RenderThread就是指渲染线程。

流畅的应用渲染需要16.6ms,但是具体16.6ms要做哪些事情。

一个Vsync的16.6ms要UIThread和RenderThread配合完成才能保证流畅的体验,UIThread是执行View的Create、Measure、Layout和Draw时候调用,即遍历View过程,RenderThread跟GPU通信会将图片上传GPU,上传图片期间UI Thread是阻塞状态的。

image.png

UIThread阻塞

UIThread被阻塞的因素多种多样,有Binder阻塞、IO阻塞等等。

Surfaceview刷新为什么用户界面没有卡顿?

因为Surfaceview拥有独立的surface Canvas,所以Surfaceview可以在开发者自定义的线程中刷新,视频刷新就不会影响到UIThread

GLSurfaceView本质上是将UI数据当成纹理,放在自定义的子线程中传入GPU后,将Bitmap数据也放到子线程,并传入GPU,即“异步纹理”,TextureView,SurfaceTexture等控件会将图片数据放在自定义的线程中渲染。

后台进程CPU高负载

如果CPU被后台进程或者线程消耗,前台应用流畅性会受到影响。

复杂View

复杂的布局会导致inflate时间变长,同时也会导致遍历View时间变长,如果遍历View和RenderThread渲染部分不能在16ms内完成会出现掉帧。

requestLayout

布局发生变化,需要重新进行measure/layout/draw的流程,requestLayout比invalidate调用更重,invalidate只是标记一个“脏区域”,不需要执行meausre/layout调用,只需要重绘即可。

requestLayout调用意味着频繁的遍历所有子View,会导致卡顿掉帧问题。

了解这些原因之后,我们就可以根据业界的APM方案定制化我们企业内部的APM方案呢。

四、业界方案

经过统计,我们发现到的卡顿问题,90%都是来自用户反馈的,我们自己发现的只有10%。

所以,我们想建设一套卡顿APM监测体系,来帮助我们在开发中,主动发现问题。

在预研了业界各个卡顿监测方案后,我们发现有几套可参考的APM技术方案分别是: BlockCanaryEx、ArgusApm、QAPM、微信广研卡顿方案、BlockCanary、QQ空间卡慢组件、美团Hertz、Blue和Matrix。

image.png

今天我们着重分析一下ArgusAPM、BlockCanary、QQ空间卡慢组件、微信广研卡顿方案和Matrix的卡顿监测原理。市面上QAPM、BlockCanary和美团Hertz是通过监测主线程Printer实现的。

ArgusAPM

比如: 360的ArgusAPM是在消息分发时候,postDelay一个 Runnable,消息分发结束移除Runnable,如果指定 delay时间内没有移除,说明发生了卡顿。

BlockCanary

比如: BlockCanary通过替换Looper的Printer实现,在每一个消息的执行前后打印日志,设置Printer后,通过两次调用println时间间隔,作为一个消息执行的耗时。去dump当前主线程执行堆栈和耗时,上报到观测平台。通过观测平台找到卡顿原因,但是打印参数有字符串拼接,性能损耗比较严重。

QQ空间卡慢组件

比如: QQ空间卡慢组件通过子线程,每隔 1 秒向主线程消息队列头部插入条空消息。假设 1 秒后消息没有被主线程消费,说明阻塞消息运行时间在 0~1 秒之间。

如果需要监测 3 秒卡顿,那么在第4次轮询中头部消息没有被消费,可以确定主线程出现一次 3 秒以上卡顿。

ArgusAPM、BlockCanary方案,可以捕获到卡顿的堆栈,最大不足在于无法获取各个函数执行耗时,很难找出稍微复杂堆栈的耗时函数,卡顿原因定位难度高。

QQ空间卡慢组件通过子线程循环获取主线程的堆栈中。

如果处理不及时,导致堆栈获取有偏移,不够准确。

如果没有耗时信息,那么卡顿更难定位。

因为获取主线程堆栈,需要暂停主线程运行,所以性能开销大。这里小木箱给大家推荐一款APM监测神器Matrix。

Matrix

比如: Matrix做法是在编译期间收集所有生成的 class 文件,扫描文件内的方法指令进行统一的打点插桩。

为了减少插桩量以及性能损耗,通过遍历 class 方法指令集,判断扫描的函数是否只含有 PUT/READ/FIELD 等简单的指令,来过滤一些默认或匿名构造函数,以及 get/set 等简单不耗时的函数。

为了方便以及高效记录函数执行过程,会为每个插桩的函数分配一个独立的 ID,在插桩过程中,记录插桩的函数签名以及分配的 ID,在插桩完成后输出一份 mapping,作为数据上报后的解析支持。

微信广研

比如: 微信广研卡顿方案通过向 Choreographer 注册监听,每一帧 doFrame 回调时判断距离上一帧的时间差是否超过阈值,如果超过了阈值即判定发生了卡顿。

将两帧之间的所有函数执行信息进行上报分析。同时,在每一帧 doFrame 到来时,重置一个计时器,如果 5s 内没有 cancel,则认为是发生了 ANR。

预研一套高维护、高可用、高扩展、可监测、可告警的卡顿APM方案并落地,同时解决采集不准、采集失效的业界痛点势在必行。

五、相关预研

我们知道造成卡顿的直接原因通常是,主线程执行繁重的UI绘制、大量的计算或IO等耗时操作。

从监测主线程的实现原理上,主要分为3大类:

  1. 主线程Printer监测
    依赖主线程 Looper,监测每次 dispatchMessage 的执行耗时(BlockCanary)。
  2. Choreographer帧率测量
    依赖 Choreographer 模块,监测相邻两次 Vsync 事件通知的时间差(LogMonitor)。
  3. 字节码插桩
    ASM字节码插桩分析慢函数耗时,超过阈值上报观测平台(Matrix)。

方案一: 主线程Printer监测

Looper.Printer基本能满足绝大部分场景,下面小木箱带大家看一下看下 Looper#loop 代码片段:

public static void loop() {
    ...
    for (;;) {
        ...
        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        msg.target.dispatchMessage(msg);
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
               }
        ...
    }
}

主线程所有执行的任务都在dispatchMessage方法中派发执行完成,我们通过setMessageLogging 的方式给主线程的Looper设置一个 Printer接口

因为dispatchMessage执行前后都会打印对应信息,在执行前利用另外一条线程,通过Thread#getStackTrace 接口,以轮询的方式获取主线程执行堆栈信息并记录起来。

同时统计每次 dispatchMessage 方法执行耗时,当超出阈值时,将该次获取的堆栈进行分析上报,从而来捕捉卡顿信息,否则丢弃此次记录的堆栈信息。

Android System提供了一个Printer接口Printer接口能在主线程执行每个绘制任务的前后都进行一次回调,Printer接口类似于iOS里runloop中的observer。

Printer接口设置给主线程后,用Printer接口在方法执行前后进行打点,然后计算出任务的执行时间,若如果超过阈值,就认为发生卡顿。就触发子线程去dump当前主线程执行堆栈和耗时,上报到观测平台。

缺陷一

问题挑战1: 堆栈采集不准

因为Printer接口方案,对卡顿的判定是需要等每个任务执行完,才去计算耗时的,而当任务执行完,再来采集堆栈,主线程可能已经开始执行下一个任务了,这个时候,采集到的堆栈,就已经不是卡顿任务的堆栈了。

image.png

问题挑战2:非耗时任务函数采集

部分场景,堆栈定位到非耗时函数,A和B相对好使,抓取几率更大,但仍然抓到C。

image.png

问题挑战3: 无响应机制

当卡顿时间超过5s,就会触发安卓系统的无响应机制,可能会强制停止app, 导致我们 ,无法采集。

解决方案: 精准采集方案

这两种问题的原因,就是在于我们依赖了主线程,所以我们的解决方案是单独建立一个计时器

每个任务开始的时候,就会触发监测线程的计时器计时当计时时间超过16ms,就会触发采集线程采集上报,不依赖任务执行为了能保证准确统计到任务最终的卡顿时间,计时器会继续计时,直到任务真正结束。

image.png

缺陷二

问题挑战: 堆栈采集失效

public final class Looper {
    private Printer mLogging;
    /**
     * Control logging of messages as they are processed by this Looper.  If
     * enabled,a log message will be written to <var>printer</var>
     * at the beginning and ending of each message dispatch,,dentifying the
     * target Handler and message contents.
     *
     * @param printer A Printer object that will receive log messages, ,
     * null to disable message logging.
     */
     public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
     }
}

不支持同时持有多个Printer实例,存在相互覆盖异常情况:

  1. 卡顿监测Printer被覆盖,导致监测方案失效
  2. 覆盖其他业务Printer,导致其他业务异常

第二个问题是方案失效,无法采集。方案失效的原因,是用来监测主线程的Printer接口,它只能绑定一个,并且任何业务都能对他设置。

因此监测Printer有可能会被,后设置的业务方所覆盖掉,导致监测失效。 同时我们也可能覆盖,别的业务,导致他们异常。

解决方案: Top堆栈精简

所以这里会存在卡顿被多次统计的情况,因此在上报的时候还会对这部分堆栈做合并,同时我们也做了配置下发,上报的时候根据后台下发的top堆栈做精简。

用了Top堆栈精简方案,可以看到,之前两种case都能准确上报,也提升了百分之5到10的堆栈收集量。并且方案本身的性能损耗很少,CPU只有1%不到,基本没有影响。

第二个问题是方案失效,无法采集。方案失效的原因,是用来监测主线程的Printer接口,它只能绑定一个,并且任何业务都能对他设置因此监测Printer有可能会被,后设置的业务方所覆盖掉,导致监测失效。

同时我可能覆盖别的业务,导致异常。所以解决方案是在设置的前和后2个时机去处理

解决方案: Printer覆盖检测

image.png

设置前检查

在设置前,我们通过反射去检查printer引用,判断是否有业务在使用,如果有,我们会保留引用,后面通过printermanager中间层去转发,在设置后,如果我们被覆盖,因为引用丢失会被java进行垃圾回收,所以我们在finalize方法监听到回收事件,然后再发起一次设置。

设置后检查

另外因为垃圾回收时间是不固定的, 所以我们还会依赖刚刚的计时器,在每次任务超时之后再进行一次检查,看它引用是否被修改,如果是再重设。

通过这2个手段我们就能解决覆盖失效的问题,提升了监测的稳定性。

方案二: Choreographer帧率测量

了解RenderThread之前,我们不得不提到Choreographer的渲染流程。

Choreographer渲染流程

  1. 主线程处于 Sleep 状态,等待 Vsync 信号
  2. Vsync 信号到来,主线程被唤醒,Choreographer 回调 FrameDisplayEventReceiver.onVsync 开始一帧的绘制
  3. 处理 App 这一帧的 Input 事件(如果有的话)
  4. 处理 App 这一帧的 Animation 事件(如果有的话)
  5. 处理 App 这一帧的 Traversal 事件(如果有的话)
  6. 主线程与渲染线程同步渲染数据,同步结束后,主线程结束一帧的绘制,可以继续处理下一个 Message(如果有的话,IdleHandler 如果不为空,这时候也会触发处理),或者进入 Sleep 状态等待下一个 Vsync。
  7. 渲染线程首先需要从 BufferQueue 里面取一个 Buffer(dequeueBuffer) ,进行数据处理之后,调用 OpenGL 相关的函数,真正地进行渲染操作,然后将渲染好的 Buffer 还给 BufferQueue (queueBuffer) ,SurfaceFlinger 在 Vsync-SF 到了之后,将所有准备好的 Buffer 取出进行合成,如下图:

image.png

图片来源: 高爷: Android Systrace 基础知识 - MainThread 和 RenderThread 解读

Choreographer测量流程

利用系统 Choreographer 模块,向该模块注册一个 FrameCallback 监听对象,同时通过另外一条线程循环记录主线程堆栈信息,并在每次 Vsync 事件 doFrame 通知回来时,循环注册该监听对象,间接统计两次 Vsync 事件的时间间隔,当超出阈值时,取出记录的堆栈进行分析上报。

简单代码实现如下:

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    @Override    
    public void doFrame(long frameTimeNanos) {
        if(frameTimeNanos - mLastFrameNanos > 100) {
            ...
        }
        mLastFrameNanos = frameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }
});

可以较方便的捕捉到卡顿的堆栈,但其最大的不足在于,无法获取到各个函数的执行耗时,对于稍微复杂一点的堆栈,很难找出可能耗时的函数,也就很难找到卡顿的原因。

另外,通过其他线程循环获取主线程的堆栈,如果稍微处理不及时,很容易导致获取的堆栈有所偏移,不够准确,加上没有耗时信息,卡顿也就不好定位。所以我们需要借助字节码插桩技术来解决这一个痛点问题。

@Override
public void dispatchBegin(long beginNs, long cpuBeginMs, long token) {
    super.dispatchBegin(beginNs, cpuBeginMs, token);
    // 记录当前方法执行的sIndex,单链表
    indexRecord = AppMethodBeat.getInstance().maskIndex("EvilMethodTracer#dispatchBegin");
}
@Override
public void dispatchEnd(long beginNs, long cpuBeginMs, long endNs, long cpuEndMs, long token, boolean isVsyncFrame) {
    super.dispatchEnd(beginNs, cpuBeginMs, endNs, cpuEndMs, token, isVsyncFrame);
    long start = config.isDevEnv() ? System.currentTimeMillis() : 0;
    long dispatchCost = (endNs - beginNs) / Constants.TIME_MILLIS_TO_NANO;
    try {
       // 超出时间,解析上传
        if (dispatchCost >= evilThresholdMs) {
            long[] data = AppMethodBeat.getInstance().copyData(indexRecord);
            long[] queueCosts = new long[3];
            System.arraycopy(queueTypeCosts, 0, queueCosts, 0, 3);
            String scene = AppActiveMatrixDelegate.INSTANCE.getVisibleScene();
            MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, queueCosts, cpuEndMs - cpuBeginMs, dispatchCost, endNs / Constants.TIME_MILLIS_TO_NANO));
        }
    } finally {
        indexRecord.release();
        if (config.isDevEnv()) {
            String usage = Utils.calculateCpuUsage(cpuEndMs - cpuBeginMs, dispatchCost);
            MatrixLog.v(TAG, "[dispatchEnd] token:%s cost:%sms cpu:%sms usage:%s innerCost:%s",
                    token, dispatchCost, cpuEndMs - cpuBeginMs, usage, System.currentTimeMillis() - start);
        }
    }
}

方案三: 字节码插桩

image.png

采用方法插桩监听每个方法的耗时,通过设置Looper的Printer,来监听监听每个Message的耗时,超过阀值,触发方法上报。

通过代理编译期间的任务 transformClassesWithDexTask,将全局 class 文件作为输入,利用 ASM 工具,高效地对所有 class 文件进行扫描及插桩。

修改字节码的方式,在编译期修改所有 class 文件中的函数字节码,对所有函数前后进行打点插桩。不过,有四点需要注意一下:

  1. 选择在该编译任务执行时插桩,是因为 proguard 操作是在该任务之前就完成的,意味着插桩时的 class 文件已经被混淆过的。
  2. 选择 proguard 之后去插桩,是因为如果提前插桩会造成部分方法不符合内联规则,没法在 proguard 时进行优化,最终导致程序方法数无法减少,从而引发方法数过大问题。
  3. 为了减少插桩量及性能损耗,通过遍历 class 方法指令集,判断扫描的函数是否只含有 PUT/READ FIELD 等简单的指令,来过滤一些默认或匿名构造函数,以及 get/set 等简单不耗时函数。
  4. 为了方便及高效记录函数执行过程,我们为每个插桩的函数分配一个独立 ID,在插桩过程中,记录插桩的函数签名及分配的 ID,在插桩完成后输出一份 mapping,作为数据上报后的解析支持。
相关实践学习
通过云拨测对指定服务器进行Ping/DNS监测
本实验将通过云拨测对指定服务器进行Ping/DNS监测,评估网站服务质量和用户体验。
相关文章
|
23天前
|
Java Android开发 UED
🧠Android多线程与异步编程实战!告别卡顿,让应用响应如丝般顺滑!🧵
【7月更文挑战第28天】在Android开发中,确保UI流畅性至关重要。多线程与异步编程技术可将耗时操作移至后台,避免阻塞主线程。我们通常采用`Thread`类、`Handler`与`Looper`、`AsyncTask`及`ExecutorService`等进行多线程编程。
35 2
|
6天前
|
Web App开发 网络协议 Android开发
### 惊天对决!Android平台一对一音视频通话方案大比拼:WebRTC VS RTMP VS RTSP,谁才是王者?
【8月更文挑战第14天】随着移动互联网的发展,实时音视频通信已成为移动应用的关键部分。本文对比分析了Android平台上WebRTC、RTMP与RTSP三种主流技术方案。WebRTC提供端到端加密与直接数据传输,适于高质量低延迟通信;RTMP适用于直播场景,但需服务器中转;RTSP支持实时流播放,但在复杂网络下稳定性不及WebRTC。三种方案各有优劣,WebRTC功能强大但集成复杂,RTMP和RTSP实现较简单但需额外编码支持。本文还提供了示例代码以帮助开发者更好地理解和应用这些技术。
23 0
|
2月前
|
XML 监控 安全
Android App性能优化之卡顿监控和卡顿优化
本文探讨了Android应用的卡顿优化,重点在于布局优化。建议包括将耗时操作移到后台、使用ViewPager2实现懒加载、减少布局嵌套并利用merge标签、使用ViewStub减少资源消耗,以及通过Layout Inspector和GPU过度绘制检测来优化。推荐使用AsyncLayoutInflater异步加载布局,但需注意线程安全和不支持特性。卡顿监控方面,提到了通过Looper、ChoreographerHelper、adb命令及第三方工具如systrace和BlockCanary。总结了Choreographer基于掉帧计算和BlockCanary基于Looper监控的原理。
46 3
|
3月前
|
存储 Android开发
Android 解决USB TP驱动中触摸卡顿和防抖动问题
Android 解决USB TP驱动中触摸卡顿和防抖动问题
93 1
|
3月前
|
Android开发
Android 新建一个lunch项(全志方案)
Android 新建一个lunch项(全志方案)
49 0
|
3月前
|
存储 XML 编译器
【Android 从入门到出门】第二章:使用声明式UI创建屏幕并探索组合原则
【Android 从入门到出门】第二章:使用声明式UI创建屏幕并探索组合原则
80 3
|
9月前
|
Android开发
[√]Android 通过adb内存监测方法
[√]Android 通过adb内存监测方法
434 1
|
11月前
|
存储 缓存 前端开发
Android Github 上面优秀的两种阴影方案,完美兼容高低版本问题
Android Github 上面优秀的两种阴影方案,完美兼容高低版本问题
|
Web App开发 编解码 网络协议
Android平台一对一音视频通话方案对比:WebRTC VS RTMP VS RTSP
Android平台一对一音视频通话方案对比:WebRTC VS RTMP VS RTSP
332 0