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

Tips: 重视微信公众号 小木箱生长营,回复 “卡顿监测” 可获得卡顿监测免费思维导图

一、引言

Hello,我是小木箱,欢迎来到小木箱生长营系列教程,今日将共享卡顿监测 计划篇 Android卡顿监测辅导准则。小木箱从七个维度将Android卡顿监测技能计划解说清楚。

第一个维度是卡顿界说,第二个维度是卡顿原因,第三个维度是业界计划,第四个维度是相关预研,第五个维度是剖析东西,第六个维度是卡顿目标,第七个维度是监测SOP。

其间,卡顿原因首要是经过制作机制的前史演进进程,剖析了卡顿本质原因。

其间,业界计划首要是经过ArgusAPM、BlockCanary、QQ空间卡慢组件、Matrix和微信广研剖析大厂是怎样做卡顿监测的。

其间,相关预研首要是解说了主线程Printer监测、Choreographer帧率丈量和字节码插桩计划。

其间,剖析东西首要是解说了东西比照和运用指南。

其间,卡顿目标首要是教咱们怎样界说监测目标。

其间,监测SOP首要是以监测规模、上报机遇、事务降级、留意事项和计划优化五个维度解说整个监测流程。

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

假如学完小木箱卡顿监测的计划篇,那么任何人做Android卡顿监测都能够拿到成果。

二、 卡顿界说

什么是卡顿?

Android5.0及以上体系中,假如主线程 + 烘托线程每一帧的履行都超越 16.6ms(60fps 的情况下),那么就或许会呈现掉帧,这便是咱们俗称的卡顿。

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

什么是卡死?

假如界面线程被堵塞超越几秒钟时刻,那么用户或许会看到ANR对话框,这便是咱们俗称的卡死。

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

卡顿原因

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

假如想要回答APP为什么滑动卡顿、不流通? 什么情况下运用会卡? 那么需求先简略了解安卓的屏幕改写机制:

制作机制前史演进

Android的制作机制大约经历了以下四个阶段:

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

混沌时代(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中。

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

洪荒时代(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 来进行烘托:

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

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

烘托进程是堵塞的,当View.Draw完结遍历,进入一个烘托信息同步的进程,会把主线程记载的制作信息同步到烘托线程。

当制作信息同步完毕,主线程会从头唤醒,依据记载的制作指令,调用openGLRender/Vulkan接口与GPU通讯,制作指令同步给GPU,GPU依据制作指令生成Graphic Buffer数据,Graphic Buffer数据交给SurfaceFlinger组成,终究显现在DisPlayer中。

假如ViewGroup控件有子View, 调用invalidate办法,当时的View才会被重绘,处理了3.0以前体系不必要的烘托问题。

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

什么是RenderNode?

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

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

什么是DisplayList?

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

RenderNode调用Canvas时,申请一个DisplayListCanvas并把详细的操作缓存到View的DrawOp树中, 接着将View缓存中的DrawOp树同步到RenderNode中,终究,遍历一切View进行制作,当时根视图树制作操作叫DisplayList。

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

为什么要用到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

那么硬件加快和软件制作有什么区别呢?

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

硬件加快和软件制作能够经过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。

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

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

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

什么是屏幕改写频率?

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

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

什么是帧率?

GPU 在一秒内制作操作的帧数,单位 fps,Android 体系则选用愈加流通的60FPS,即每秒钟GPU最多制作 60 帧画面。

帧率是动态变化的,例如当画面静止时,GPU 是没有制作操作的,屏幕改写的还是Buffer中的数据,即GPU终究操作的帧数据。

FPS 能够衡量一个界面的流通性,但往往不能很直观的衡量卡顿的产生。一个安稳在 40、50 FPS 的页面,咱们不会认为是卡顿的,但一旦 FPS 很不安稳,人眼往往很简略感知到,因而咱们能够经过掉帧程度来衡量卡顿。 业界都是运用 Choreographer 来监听运用的帧率,跟卡顿不同的是,需求排除掉页面没有操作的情况,咱们应该只在界面存在制作的时分做核算。

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

怎样监听界面是否存在制作行为呢?

怎样监听界面是否存在制作行为呢?能够经过 addOnDrawListener 完成

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

什么是画面撕裂问题?

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

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

怎样处理画面撕裂问题?

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

引入了双缓概念, 当屏幕改写时,Back Buffer并不会产生变化,后台Back Buffer预备就绪后,Back Buffer和Frame Buffer才进行交流。

什么是双缓冲?

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

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

图片来历于: 高爷

Back Buffer和Frame Buffer的交流机遇是什么时分呢?

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

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

怎样了解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缓冲区。

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

尽管Graphic Buffer 占用了内存, 而且比较双缓冲区有所推迟,可是Back Buffer、Frame Buffer和Graphic Buffer三缓冲有用运用了等候Vsync的时刻,削减了丢帧。以上, 便是Android屏幕改写原理

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

末法时代(5.0后): RenderThread

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

由于View本身创立、制作杂乱和主线程被堵塞,无法及时制作等原因,而16.6ms内需求完结UI创立、制作、烘托、上屏。

影响CPU的大头是UIThread, 分别对应着View的Create、Measure、Layout和Draw ,为此在16.6ms内,进步View的制作功率是处理卡顿问题的要害。

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

非UIThread的履行逻辑导致的卡顿需求依据详细事务场景剖析,比方影视播放卡顿或许是播放器原因,或许是网络原因等等。UIThread和RenderThread里边的卡顿有如下几类的原因:

RenderThread堵塞

什么是RenderThread?

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

所谓的有可见内容的时分进行烘托的线程,RenderThread便是指烘托线程。

流通的运用烘托需求16.6ms,可是详细16.6ms要做哪些事情。

一个Vsync的16.6ms要UIThread和RenderThread配合完结才干保证流通的体验,UIThread是履行View的Create、Measure、Layout和Draw时分调用,即遍历View进程,RenderThread跟GPU通讯会将图片上传GPU,上传图片期间UI Thread是堵塞状况的。

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

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。

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

今日咱们侧重剖析一下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接口计划,对卡顿的断定是需求等每个使命履行完,才去核算耗时的,而当使命履行完,再来搜集仓库,主线程或许现已开端履行下一个使命了,这个时分,搜集到的仓库,就现已不是卡顿使命的仓库了。

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

问题应战2:非耗时使命函数搜集

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

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

问题应战3: 无呼应机制

当卡顿时刻超越5s,就会触发安卓体系的无呼应机制,或许会强制中止app, 导致咱们 ,无法搜集。

处理计划: 精准搜集计划

这两种问题的原因,便是在于咱们依靠了主线程,所以咱们的处理计划是单独建立一个计时器

每个使命开端的时分,就会触发监测线程的计时器计时当计时时刻超越16ms,就会触发搜集线程搜集上报,不依靠使命履行为了能保证精确核算到使命终究的卡顿时刻,计时器会持续计时,直到使命真正完毕。

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

缺点二

问题应战: 仓库搜集失效

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掩盖检测

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

设置前检查

在设置前,咱们经过反射去检查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 取出进行组成,如下图:

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

图片来历: 高爷: 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, 03);
            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);
        }
    }
}

计划三: 字节码插桩

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

选用办法插桩监听每个办法的耗时,经过设置Looper的Printer,来监听监听每个Message的耗时,超越阀值,触发办法上报。

经过署理编译期间的使命 transformClassesWithDexTask,将全局 class 文件作为输入,运用 ASM 东西,高效地对一切 class 文件进行扫描及插桩。

修正字节码的办法,在编译期修正一切 class 文件中的函数字节码,对一切函数前后进行打点插桩。不过,有四点需求留意一下:

  1. 挑选在该编译使命履行时插桩,是由于 proguard 操作是在该使命之前就完结的,意味着插桩时的 class 文件现已被混淆过的。

  2. 挑选 proguard 之后去插桩,是由于假如提早插桩会形成部分办法不符合内联规则,没法在 proguard 时进行优化,终究导致程序办法数无法削减,然后引发办法数过大问题。

  1. 为了削减插桩量及功能损耗,经过遍历 class 办法指令集,判别扫描的函数是否只含有 PUT/READ FIELD 等简略的指令,来过滤一些默许或匿名构造函数,以及 get/set 等简略不耗时函数。
  1. 为了便利及高效记载函数履行进程,咱们为每个插桩的函数分配一个独立 ID,在插桩进程中,记载插桩的函数签名及分配的 ID,在插桩完结后输出一份 mapping,作为数据上报后的解析支撑。

六、 剖析东西

东西比照

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

运用指南

  • 需求剖析Native代码,选Simpleperf
  • 需求剖析体系调用,选Systrace
  • 需求剖析运用流程和耗时,选TraceView / 插桩之后的Systrace
  • 需求剖析其他运用,选Nanoscope
  • 灰度环境剖析Java代码,选Rhea

七、 卡顿目标

怎样用一个目标直观反映卡顿呢?

监测目标

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

流通Smoothness核算

  1. 当Vsync信号抵达时会会调用HAL层的HWComposer.vsync()函数,告诉HWComposer引擎进行GPU烘托和显现,,然后发送Vsync信号给SurfaceFinger处理

  2. SurfaceFinger接收到Vsync信号后,调用SurfaceFlinger的addResyncSample函数用来处理 Vsync 信号,addResyncSample函数能够将App的烘托帧同步到显现器的改写时刻,以防止呈现撕裂和卡顿等问题。

  3. EventThread经过onVsyncEvent函数将Vsync信号分发给需求运用Vsync信号的App,完成更平滑和流通的烘托效果

  4. 当体系收到显现器的 Vsync 信号时,DisplayEventReceiver.onVsync() 函数会被调用,并将时刻戳和Displayer物理特色传递给App

  5. App收到时刻戳和Displayer物理特色后,FrameHandler能够协助运用程序将烘托帧与 Vsync 信号同步,当 FrameHandler 接收到 Vsync 信号时,FrameHandler会调用 sendMessage() 办法,并将帧同步音讯作为参数传递给该办法

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

流通度存在哪些痛点问题?

流通度存在两个痛点:

第一个痛点是: 数据量大,不便利核算,导致淹没实在卡顿Case。

第二个痛点是: 不能简略运用平均值和方差。由于不同设备的卡顿规范线不一样,咱们应该按照设备等级划分标注线

流通度目标是怎样衡量的?

  • 目标一: 流通度评分

    • 压缩数据
    • 加权放大卡顿

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

  • 目标二: XPM评分(denzelzhou):离散程度

    • 帧制作时长到规范制作时长的间隔(点到线的间隔)
    • 间隔规范制作时长越远就越卡顿

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

八、监测SOP

监测规模

慢函数监测

技能需求: 经过外部装备阈值记载Android慢函数,假如超越阈值,那么将慢函数办法名和耗时刻信息记载在本地JSON文件

思路:

  1. 首要界说一个类 SlowFunctionClassVisitor,承继自 ClassVisitor,用于完成对类的字节码的修正。
  2. 在 SlowFunctionClassVisitor 中重写 visitMethod 办法,用于完成对办法的字节码的修正。在 visitMethod 办法中,先调用父类的 visitMethod 办法,然后运用 ASM 的 API 生成新的办法字节码,并将原来的办法字节码替换为新的办法字节码。
  3. 在生成新的办法字节码时,运用 Label 和 JumpInsnNode 等 ASM 的 API 刺进代码,完成对办法的耗时进行判别。假如办法耗时超越阈值,则记载慢函数信息,并将其写入本地 JSON 文件中。
  4. 为了完成记载慢函数信息和将其写入本地 JSON 文件中,运用了 org.json.JSONObject 和 java.io.FileWriter 等相关的 API。

假定咱们要记载的慢函数是指履行时刻超越100ms的函数,而且阈值的装备办法是经过一个装备文件,其间包括一个键值对 “slow_function_threshold=100″,存放在assets目录下的config.json文件中。

首要,在Android Studio中创立一个新的Android项目,并将以下代码添加到build.gradle文件的dependencies块中:

dependencies {
    implementation 'org.ow2.asm:asm:9.2'
    implementation 'org.ow2.asm:asm-util:9.2'
}

接下来,咱们需求创立一个ASM的ClassVisitor,用于在办法调用前后刺进代码。


public class SlowFunctionClassVisitor extends ClassVisitor {
    private String className;
    public SlowFunctionClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM9, classVisitor);
    }
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        className = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        return new SlowFunctionMethodVisitor(Opcodes.ASM9, mv, access, name, descriptor, className);
    }
    private static class SlowFunctionMethodVisitor extends AdviceAdapter {
        private final String methodName;
        private final String className;
        protected SlowFunctionMethodVisitor(int api, MethodVisitor mv, int access, String name, String descriptor, String className) {
            super(api, mv, access, name, descriptor);
            this.methodName = name;
            this.className = className;
        }
        private static final String startTimeFieldName = "_start_time";
        @Override
        protected void onMethodEnter() {
            //在办法进入时刺进代码
            visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            visitLdcInsn("enter " + methodName);
            visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            visitVarInsn(Opcodes.ALOAD, 0);
            visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            visitFieldInsn(Opcodes.PUTFIELD, className, startTimeFieldName, "J");
        }
        @Override
        protected void onMethodExit(int opcode) {
            //在办法退出时刺进代码
            visitVarInsn(Opcodes.ALOAD, 0);
            visitFieldInsn(Opcodes.GETFIELD, className, startTimeFieldName, "J");
            visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            visitInsn(Opcodes.LSUB);
            visitVarInsn(Opcodes.LSTORE, 2);
            Label l1 = new Label();
            visitVarInsn(Opcodes.LLOAD, 2);
        visitLdcInsn(100L);  // 100ms
        visitInsn(Opcodes.LCMP);
        visitJumpInsn(Opcodes.IFLE, l1);
        // 超越阈值,记载慢函数信息
        visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        visitLdcInsn("exit " + methodName + " cost " + Long.toString(2L) + " ms");
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        // 加载阈值
        visitLdcInsn("slow_function_threshold");
        visitMethodInsn(Opcodes.INVOKESTATIC, "android/content/res/Resources", "getSystem", "()Landroid/content/res/Resources;", false);
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/content/res/Resources", "getAssets", "()Landroid/content/res/AssetManager;", false);
        visitLdcInsn("config.json");
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/content/res/AssetManager", "open", "(Ljava/lang/String;)Ljava/io/InputStream;", false);
        visitTypeInsn(Opcodes.NEW, "org/json/JSONObject");
        visitInsn(Opcodes.DUP);
        visitTypeInsn(Opcodes.NEW, "java/io/InputStreamReader");
        visitInsn(Opcodes.DUP);
        visitVarInsn(Opcodes.ALOAD, 4);
        visitMethodInsn(Opcodes.INVOKESPECIAL, "java/io/InputStreamReader", "<init>", "(Ljava/io/InputStream;)V", false);
        visitMethodInsn(Opcodes.INVOKESPECIAL, "org/json/JSONObject", "<init>", "(Ljava/io/Reader;)V", false);
        visitLdcInsn("slow_function_threshold");
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/json/JSONObject", "optInt", "(Ljava/lang/String;)I", false);
        // 比较耗时和阈值
        visitVarInsn(Opcodes.LLOAD, 2);
        visitInsn(Opcodes.LCMP);
        visitVarInsn(Opcodes.ILOAD, 5);
        Label l2 = new Label();
        visitJumpInsn(Opcodes.IF_ICMPLE, l2);
        // 超越阈值,写入本地JSON文件
        visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        visitLdcInsn("write to local json file: " + methodName + " cost " + Long.toString(2L) + " ms");
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        visitTypeInsn(Opcodes.NEW, "org/json/JSONObject");
        visitInsn(Opcodes.DUP);
        visitVarInsn(Opcodes.ALOAD, 0);
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
        visitLdcInsn(methodName);
        visitVarInsn(Opcodes.LLOAD, 2);
        visitMethodInsn(Opcodes.INVOKESPECIAL, "org/json/JSONObject", "<init>", "()V", false);
        visitVarInsn(Opcodes.ASTORE, 6);
        visitTypeInsn(Opcodes.NEW, "java/io/FileWriter");
        visitInsn(Opcodes.DUP);
        visitLdcInsn("slow_function.json");
        visitMethodInsn(Opcodes.INVOKESPECIAL, "java/io/FileWriter", "<init>", "(Ljava/lang/String;)V", false);
        visitVarInsn(Opcodes.ASTORE, 7);
        // 将慢函数信息写入JSON文件
        visitVarInsn(Opcodes.ALOAD, 7);
        visitVarInsn(Opcodes.ALOAD, 6);
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/json/JSONObject", "toString", "()Ljava/lang/String;", false);
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/FileWriter", "write", "(Ljava/lang/String;)V", false);
        visitVarInsn(Opcodes.ALOAD, 7);
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/FileWriter", "flush", "()V", false);
        visitVarInsn(Opcodes.ALOAD, 7);
        visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/FileWriter", "close", "()V", false);
        visitLabel(l2);
    }
    super.visitInsn(opcode);
}
}

以上是一段运用ASM技能完成记载Android慢函数的代码。它经过在办法的字节码中刺进代码来判别办法耗时,假如超越阈值,就记载慢函数信息,并将其写入本地JSON文件中。在完成进程中,运用了ASM的相关API来生成字节码,并在办法履行进程中进行修正。

流通性监测

Activity、Service、Receiver 组件生命周期的耗时和调用次数也是咱们要点重视的功能问题。

例如Activity的onCreate不应该超越 1 秒,不然会影响用户看到页面的时刻。

Service 和 Receiver 尽管是后台组件,不过它们的生命周期也是占用主线程的,也是咱们需求重视的问题。 关于组件生命周期咱们应该选用更严格的监测,能够全量上报各个组件各个生命周期的发动时刻和发动次数。 一般的做法是,经过编译时插桩来做到组件的生命周期监测。

FPS监测

咱们需求搜集的卡顿数据有: 卡顿次数/交互次数比,帧制作时刻采样. 处理办法能够参阅下面的内容:

  1. 界说一个FPS监测类,该类中包括FPS核算的逻辑:
public class FPSMonitor {
    private static final long ONE_SECOND = 1000000000L;
    private long lastTime = System.nanoTime();
    private int frameCount = 0;
    private int fps = 0;
    public void update() {
        long currentTime = System.nanoTime();
        frameCount++;
        if (currentTime - lastTime >= ONE_SECOND) {
            fps = frameCount;
            frameCount = 0;
            lastTime = currentTime;
        }
    }
    public int getFps() {
        return fps;
    }
}
  1. 运用ASM字节码框架在编译期间对代码进行插桩,将FPS监测的逻辑刺进到游戏或运用程序的主循环中:
public class GameLoop {
    private FPSMonitor fpsMonitor = new FPSMonitor();
    public void loop() {
        while (true) {
            long startTime = System.nanoTime();
            // 游戏或运用程序的主逻辑// ...// 在主循环中刺进FPS监测逻辑
            fpsMonitor.update();
            int fps = fpsMonitor.getFps();
            System.out.println("FPS: " + fps);
            // 操控FPS为60long elapsedTime = System.nanoTime() - startTime;
            long sleepTime = (1000000000L / 60) - elapsedTime;
            if (sleepTime > 0) {
                try {
                    Thread.sleep(sleepTime / 1000000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  1. FPS监测逻辑被刺进到了运用程序的主循环中,在每一帧完毕时核算FPS,并将核算成果输出到操控台。咱们运用插件能够将上述代码转化成字节码文件。转化代码如下:
public class FpsMonitorClassVisitor extends ClassVisitor {
    public FpsMonitorClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        if (name.equals("loop")) {
            mv = new FpsMonitorMethodVisitor(mv);
        }
        return mv;
    }
    private static class FpsMonitorMethodVisitor extends MethodVisitor {
        public FpsMonitorMethodVisitor(MethodVisitor mv) {
            super(Opcodes.ASM5, mv);
        }
        @Override
        public void visitCode() {
            super.visitCode();
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitFieldInsn(Opcodes.GETFIELD, "GameLoop", "fpsMonitor", "LFPSMonitor;");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "FPSMonitor", "update", "()V", false);
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitFieldInsn(Opcodes.GETFIELD, "GameLoop", "fpsMonitor", "LFPSMonitor;");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "FPSMonitor", "getFps", "()I", false);
            mv.visitFieldInsn(Opcodes.PUTSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitInsn(Opcodes.SWAP);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
        }
    }
}

创立了一个 FpsMonitorClassVisitor 类来处理插桩逻辑,它承继自 ClassVisitor 类。

visitMethod 办法中,咱们判别当时拜访的办法是否为 loop 办法,假如是,则创立一个 FpsMonitorMethodVisitor 对象来处理该办法的插桩逻辑。

FpsMonitorMethodVisitor 中,咱们将 FPS 监测逻辑刺进到了游戏或运用程序的主循环中,在每一帧完毕时核算 FPS,并将核算成果输出到操控台。

终究,咱们将创立的 FpsMonitorClassVisitor 对象传递给 ASM 的 ClassReader,并经过 ClassWriter 来生成修正后的字节码。

Thread监测

由于文件IO开支、线程间的竞赛或许锁或许会导致主线程空等,然后导致卡顿。咱们能够凭借BHook对线程进行监测,需求监测以下两点:

线程数量

需求监测线程数量的多少,以及创立线程的办法。例如有没有运用一致的线程池,这块能够经过 hook 线程的 nativeCreate() 函数,首要用于进行线程收敛,削减线程数量。

线程时刻

监测线程的用户时刻 utime、体系时刻 stime 和优先级。首要是看哪些线程 utime+stime 时刻比较多,占用了过多的 CPU。

上报机遇

超越设置慢函数的阈值,开端搜集慢办法函数和时刻,开端上报。

事务降级

可经过参数装备渠道装备开关,随时回归线上版别

留意事项

插桩计划问题 处理计划
包巨细添加多少 过滤简略函数:i++,getter/setter支撑黑名单装备包巨细操控2%
只能监测到运用仓库耗时,无法搜集体系仓库耗时 结合Looper计划,一起上报后台依据卡顿key来聚合剖析
APP功能影响规模 非事务模块不要插桩,防止对安稳性形成影响

计划优化

为了进步本身功能,咱们是不是能够同步线程办法进一步优化获取仓库功能

getStackTrace

getStackTrace获取仓库信息有如下两大事务痛点:

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

  1. 功能损耗

  2. 需求暂停主线程运转

为了处理上述事务痛点,咱们能够采取ThreadDump和AsyncGetCallTrace两种处理计划。

计划 完成 特色
StackSampler 依靠SafePoint搜集 公开API,安稳,有中止
AsyncGetCallTrace SIGPROF定时器,Native层搜集仓库 非公开API,兼容性问题无中止

StackSampler

在SafePoint处,JVM能够保证一切线程都中止履行,然后保证当时状况的一致性。运用SafePoint,咱们能够完成无需挂起主线程的异步仓库采样,然后防止了主线程被挂起的影响。

public class StackSampler {
    private static final int MAX_STACK_DEPTH = 32; // 最大仓库深度
    private static final int MAX_STACK_SAMPLES = 100; // 最大缓存采样数
    private static final long SAMPLE_INTERVAL = 100L; // 采样间隔,单位:毫秒
    private final ConcurrentLinkedQueue<String> stackSamples; // 仓库采样缓存
    private final AtomicBoolean profiling; // 采样标志位
    public StackSampler() {
        stackSamples = new ConcurrentLinkedQueue<>();
        profiling = new AtomicBoolean(false);
    }
    // SafePoint 采样办法
    private void sample() {
        if (profiling.compareAndSet(falsetrue)) {
            // 在 SafePoint 处异步采样仓库信息
            new Thread(() -> {
                // 推迟一段时刻,等候一切线程进入 SafePoint
                try {
                    Thread.sleep(SAMPLE_INTERVAL);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 采样仓库信息并添加到缓存中
                String stackTrace = getStackTrace();
                stackSamples.offer(stackTrace);
                // 重置采样标志位
                profiling.set(false);
            }).start();
        }
    }
    // 获取当时线程的仓库信息
    private String getStackTrace() {
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        StringBuilder sb = new StringBuilder();
        for (int i = 2; i < Math.min(stackTrace.length,,AX_STACK_DEPTH + 2); i++) {
            sb.append(stackTrace[i].toString()).append('\n');
        }
        return sb.toString();
    }
    // 输出一切采样成果
    public void dump() {
        int count = 0;
        String stackTrace;
        while ((stackTrace = stackSamples.poll()) != null && count < MAX_STACK_SAMPLES) {
            System.out.println(stackTrace);
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        StackSampler sampler = new StackSampler();
        sampler.sample(); // 开端采样
        Thread.sleep(MAX_STACK_SAMPLES * SAMPLE_INTERVAL); // 等候采样完结
        sampler.dump(); // 输出采样成果
    }
}

用SafePoint完成了异步仓库采样,并在每次采样时推迟一段时刻,等候一切线程进入SafePoint,以保证采样的仓库信息是当时线程的实在状况。一起,采样的成果存储在一个线程安全的行列中,等候输出。当采样完结后,经过 dump() 办法输出一切的采样。

别的,为了防止频繁地采样仓库信息导致功能问题,咱们能够经过削减采样频率的办法进行优化。例如,能够经过将采样间隔从10毫秒调整为100毫秒,来削减采样的次数,然后下降对体系功能的影响。

别的,为了更高效地采样和处理仓库信息,咱们能够考虑选用缓存和批量处理的战略。例如,能够将采样的成果存储在一个缓存行列中,当行列到达必定巨细时再进行批量处理,然后削减对行列的频繁拜访和操作,进步程序的履行功率。

AsyncGetCallTrace

经过运用SIGPROF定时器,Native层搜集仓库办法,优化getStackTrace获取仓库需求暂停主线程运转和相关功能问题

#include <signal.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <execinfo.h>
#define SAMPLE_INTERVAL 100 // 采样间隔,单位:毫秒
#define MAX_STACK_DEPTH 32 // 最大仓库深度
#define MAX_STACK_SAMPLES 100 // 最大缓存采样数
volatile sig_atomic_t profiling = 0; // 采样标志位
// SIGPROF 信号处理函数
void profiling_handler(int signum) {
    profiling = 1; // 符号需求采样仓库信息
}
// 开端采样
void start_profiling() {
    struct sigaction sa;
    struct itimerval timer;
    // 注册 SIGPROF 信号处理函数
    sa.sa_handler = profiling_handler;
    sa.sa_flags = SA_RESTART;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGPROF, &sa, NULL);
    // 设置定时器
    timer.it_value.tv_sec = SAMPLE_INTERVAL / 1000;
    timer.it_value.tv_usec = (SAMPLE_INTERVAL % 1000) * 1000;
    timer.it_interval = timer.it_value;
    setitimer(ITIMER_PROF, &timer, NULL);
}
// 中止采样
void stop_profiling() {
    struct itimerval timer;
    // 封闭定时器
    timer.it_value.tv_sec = 0;
    timer.it_value.tv_usec = 0;
    timer.it_interval = timer.it_value;
    setitimer(ITIMER_PROF, &timer, NULL);
}
// 搜集仓库信息并输出到规范输出流
void dump_stack() {
    void *stack[MAX_STACK_DEPTH];
    int depth = backtrace(stack, MAX_STACK_DEPTH);
    if (depth > 0) {
        backtrace_symbols_fd(stack, depth, STDOUT_FILENO);
    }
}
int main() {
    start_profiling(); // 开端采样
    int sample_count = 0;
    while (sample_count < MAX_STACK_SAMPLES) {
        if (profiling) { // 假如需求采样仓库信息
            profiling = 0; // 重置采样标志位
            dump_stack(); // 搜集仓库信息
            sample_count++; // 核算采样数
        }
    }
    stop_profiling(); // 中止采样
    return 0;
}

start_profiling()stop_profiling() 函数用于敞开和封闭 SIGPROF 定时器,定时器的时刻间隔由 SAMPLE_INTERVAL 宏界说指定。

profiling_handler() 函数是 SIGPROF 信号的处理函数,每次接收到信号后,将 profiling 变量置为 1。dump_stack() 函数用于搜集仓库信息,经过 backtrace() 函数获取仓库信息,并输出到规范输出流中。

main() 函数中,循环搜集仓库信息,直到采样数到达最大值停止,最大采样数由 MAX_STACK_SAMPLES 宏界说指定。

经过 SIGPROF 定时器和信号处理函数的办法,能够在 Native 层搜集仓库信息,防止了在 Java 层获取仓库信息的开支和功能问题。不过需求留意的是,仓库采样对运用程序的功能有必定影响,需求权衡好采样间隔和采样深度等参数

九、总结展望

当测验提出卡顿问题,测验会新建Bug单给责任人处理。导致卡顿的原因有很多,比方函数十分耗时、I/O 十分慢、线程或锁间竞赛等。

随着移动端用户越来越重视产品体验,APM体系也逐步成为互联网公司重要基础设施。

卡顿是衡量App功能的一个重要目标,建设卡顿APM监测渠道是Android卡顿优化长效管理要害。

一起,经过建设卡顿APM监测渠道,协助事务找到卡顿原因也是架构组TL查核评测职工要点OKR指向。

为了处理卡顿规范不明确问题,小木箱今日和咱们侧重探讨了卡顿监测的方方面面。

假如小木箱的文章对你有所协助,那么欢迎重视小木箱的公众号: 小木箱生长营。我是小木箱,咱们下一篇见~

参阅链接

  • github.com/Tencent/mat…
  • gityuan.com/2017/02/25/…
  • www.jianshu.com/p/9e8f88eac…