友情链接: BaguTree《Android 面试、卡顿、ANR》共享
Tips: 重视微信公众号 小木箱生长营,回复 “卡顿监测” 可获得卡顿监测免费思维导图
一、引言
Hello,我是小木箱,欢迎来到小木箱生长营系列教程,今日将共享卡顿监测 计划篇 Android卡顿监测辅导准则。小木箱从七个维度将Android卡顿监测技能计划解说清楚。
第一个维度是卡顿界说,第二个维度是卡顿原因,第三个维度是业界计划,第四个维度是相关预研,第五个维度是剖析东西,第六个维度是卡顿目标,第七个维度是监测SOP。
其间,卡顿原因首要是经过制作机制的前史演进进程,剖析了卡顿本质原因。
其间,业界计划首要是经过ArgusAPM、BlockCanary、QQ空间卡慢组件、Matrix和微信广研剖析大厂是怎样做卡顿监测的。
其间,相关预研首要是解说了主线程Printer监测、Choreographer帧率丈量和字节码插桩计划。
其间,剖析东西首要是解说了东西比照和运用指南。
其间,卡顿目标首要是教咱们怎样界说监测目标。
其间,监测SOP首要是以监测规模、上报机遇、事务降级、留意事项和计划优化五个维度解说整个监测流程。
假如学完小木箱卡顿监测的计划篇,那么任何人做Android卡顿监测都能够拿到成果。
二、 卡顿界说
什么是卡顿?
Android5.0及以上体系中,假如主线程 + 烘托线程每一帧的履行都超越 16.6ms(60fps 的情况下),那么就或许会呈现掉帧,这便是咱们俗称的卡顿。
什么是卡死?
假如界面线程被堵塞超越几秒钟时刻,那么用户或许会看到ANR对话框,这便是咱们俗称的卡死。
三 、 卡顿原因
APP为什么滑动卡顿、不流通? 什么情况下运用会卡?
假如想要回答APP为什么滑动卡顿、不流通? 什么情况下运用会卡? 那么需求先简略了解安卓的屏幕改写机制:
制作机制前史演进
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中。
洪荒时代(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 Systrace 基础知识 – MainThread 和 RenderThread 解读
烘托进程是堵塞的,当View.Draw完结遍历,进入一个烘托信息同步的进程,会把主线程记载的制作信息同步到烘托线程。
当制作信息同步完毕,主线程会从头唤醒,依据记载的制作指令,调用openGLRender/Vulkan接口与GPU通讯,制作指令同步给GPU,GPU依据制作指令生成Graphic Buffer数据,Graphic Buffer数据交给SurfaceFlinger组成,终究显现在DisPlayer中。
假如ViewGroup控件有子View, 调用invalidate办法,当时的View才会被重绘,处理了3.0以前体系不必要的烘托问题。
什么是RenderNode?
创立视图会创立RenderNode,硬件加快中用RenderNode标识对应视图。
什么是DisplayList?
Android 运用 DisplayList 进行制作而非直接运用 CPU 制作每一帧。DisplayList 是一系列制作操作的记载,抽象为 RenderNode 类。
RenderNode调用Canvas时,申请一个DisplayListCanvas并把详细的操作缓存到View的DrawOp树中, 接着将View缓存中的DrawOp树同步到RenderNode中,终究,遍历一切View进行制作,当时根视图树制作操作叫DisplayList。
为什么要用到DisplayList?而不是CPU直接操作?
- DisplayList 能够按需屡次制作而无须同事务逻辑交互
- 特定的制作操作(如 translation, scale 等)能够作用于整个 DisplayList 而无须从头分发制作操作
- 当知晓了一切制作操作后,能够针对其进行优化:例如,一切的文本能够一同进行制作一次
- 能够将对 DisplayList 的处理转移至另一个线程(也便是 RenderThread)
- 主线程在 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步:
-
当调用父ViewGroup的子视图的invalidate办法进行制作
-
给没有制作的子视图的DisplayList符号一个dirty
-
调用子视图的父ViewGroup的invalidate办法进行制作
-
调用视图的根节点ViewRoot的invalidate办法进行根视图制作
-
调用rebuild办法重构根视图树DisplayList
-
调用rebuild办法重构根视图树DisplayList一切子视图树DisplayList
那么硬件加快和软件制作有什么区别呢?
硬件加快和软件制作能够经过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。
上古时代(4.1~5.0): Vsync + 三缓冲区
第三个阶段是上古时代,即Android版别在4.1~5.0版别之间。
什么是屏幕改写频率?
一秒内显现了多少帧的图像,单位 Hz。
什么是帧率?
GPU 在一秒内制作操作的帧数,单位 fps,Android 体系则选用愈加流通的60FPS,即每秒钟GPU最多制作 60 帧画面。
帧率是动态变化的,例如当画面静止时,GPU 是没有制作操作的,屏幕改写的还是Buffer中的数据,即GPU终究操作的帧数据。
FPS 能够衡量一个界面的流通性,但往往不能很直观的衡量卡顿的产生。一个安稳在 40、50 FPS 的页面,咱们不会认为是卡顿的,但一旦 FPS 很不安稳,人眼往往很简略感知到,因而咱们能够经过掉帧程度来衡量卡顿。 业界都是运用 Choreographer 来监听运用的帧率,跟卡顿不同的是,需求排除掉页面没有操作的情况,咱们应该只在界面存在制作的时分做核算。
怎样监听界面是否存在制作行为呢?
怎样监听界面是否存在制作行为呢?能够经过 addOnDrawListener 完成
getWindow().getDecorView().getViewTreeObserver().addOnDrawListener()
什么是画面撕裂问题?
一个屏幕内的数据来自2个不同的帧,画面会呈现撕裂感。
怎样处理画面撕裂问题?
为了处理Android体系画面撕裂问题,即一个屏幕内的Back Buffer数据来自2个不同的帧,画面错位。
引入了双缓冲概念, 当屏幕改写时,Back Buffer并不会产生变化,后台Back Buffer预备就绪后,Back Buffer和Frame Buffer才进行交流。
什么是双缓冲?
所谓的双缓存便是CPU/GPU写数据到Back Buffer,DisPlayer从Frame Buffer取数据。
图片来历于: 高爷
Back Buffer和Frame Buffer的交流机遇是什么时分呢?
Vsync是Back Buffer数据和Frame Buffer数据进行交流的最佳时刻点。
怎样了解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缓冲区。
尽管Graphic Buffer 占用了内存, 而且比较双缓冲区有所推迟,可是Back Buffer、Frame Buffer和Graphic Buffer三缓冲有用运用了等候Vsync的时刻,削减了丢帧。以上, 便是Android屏幕改写原理 。
末法时代(5.0后): RenderThread
第三个阶段是末法时代,即Android版别在5.0今后的版别。
由于View本身创立、制作杂乱和主线程被堵塞,无法及时制作等原因,而16.6ms内需求完结UI创立、制作、烘托、上屏。
影响CPU的大头是UIThread, 分别对应着View的Create、Measure、Layout和Draw ,为此在16.6ms内,进步View的制作功率是处理卡顿问题的要害。
非UIThread的履行逻辑导致的卡顿需求依据详细事务场景剖析,比方影视播放卡顿或许是播放器原因,或许是网络原因等等。UIThread和RenderThread里边的卡顿有如下几类的原因:
RenderThread堵塞
什么是RenderThread?
所谓的有可见内容的时分进行烘托的线程,RenderThread便是指烘托线程。
流通的运用烘托需求16.6ms,可是详细16.6ms要做哪些事情。
一个Vsync的16.6ms要UIThread和RenderThread配合完结才干保证流通的体验,UIThread是履行View的Create、Measure、Layout和Draw时分调用,即遍历View进程,RenderThread跟GPU通讯会将图片上传GPU,上传图片期间UI Thread是堵塞状况的。
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。
今日咱们侧重剖析一下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大类:
-
主线程Printer监测
依靠主线程 Looper,监测每次 dispatchMessage 的履行耗时(BlockCanary)。
-
Choreographer帧率丈量
依靠 Choreographer 模块,监测相邻两次 Vsync 事情告诉的时刻差(LogMonitor)。
-
字节码插桩
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接口计划,对卡顿的断定是需求等每个使命履行完,才去核算耗时的,而当使命履行完,再来搜集仓库,主线程或许现已开端履行下一个使命了,这个时分,搜集到的仓库,就现已不是卡顿使命的仓库了。
问题应战2:非耗时使命函数搜集
部分场景,仓库定位到非耗时函数,A和B相对好使,抓取几率更大,但仍然抓到C。
问题应战3: 无呼应机制
当卡顿时刻超越5s,就会触发安卓体系的无呼应机制,或许会强制中止app, 导致咱们 ,无法搜集。
处理计划: 精准搜集计划
这两种问题的原因,便是在于咱们依靠了主线程,所以咱们的处理计划是单独建立一个计时器。
在每个使命开端的时分,就会触发监测线程的计时器计时,当计时时刻超越16ms,就会触发搜集线程搜集上报,不依靠使命履行为了能保证精确核算到使命终究的卡顿时刻,计时器会持续计时,直到使命真正完毕。
缺点二
问题应战: 仓库搜集失效
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实例,存在相互掩盖异常情况:
-
卡顿监测Printer被掩盖,导致监测计划失效
-
掩盖其他事务Printer,导致其他事务异常
第二个问题是计划失效,无法搜集。计划失效的原因,是用来监测主线程的Printer接口,它只能绑定一个,而且任何事务都能对他设置。
因而监测Printer有或许会被,后设置的事务方所掩盖掉,导致监测失效。 一起咱们也或许掩盖,其他事务,导致他们异常。
处理计划: Top仓库精简
所以这儿会存在卡顿被屡次核算的情况,因而在上报的时分还会对这部分仓库做合并,一起咱们也做了装备下发,上报的时分依据后台下发的top仓库做精简。
用了Top仓库精简计划,能够看到,之前两种case都能精确上报,也进步了百分之5到10的仓库搜集量。而且计划本身的功能损耗很少,CPU只要1%不到,基本没有影响。
第二个问题是计划失效,无法搜集。计划失效的原因,是用来监测主线程的Printer接口,它只能绑定一个,而且任何事务都能对他设置因而监测Printer有或许会被,后设置的事务方所掩盖掉,导致监测失效。
一起我或许掩盖其他事务,导致异常。所以处理计划是在设置的前和后2个机遇去处理
处理计划: Printer掩盖检测
设置前检查
在设置前,咱们经过反射去检查printer引证,判别是否有事务在运用,假如有,咱们会保存引证,后面经过printermanager中间层去转发,在设置后,假如咱们被掩盖,由于引证丢掉会被java进行废物收回,所以咱们在finalize办法监听到收回事情,然后再建议一次设置。
设置后检查
别的由于废物收回时刻是不固定的, 所以咱们还会依靠刚刚的计时器,在每次使命超时之后再进行一次检查,看它引证是否被修正,假如是再重设。
经过这2个手法咱们就能处理掩盖失效的问题,进步了监测的安稳性。
计划二: Choreographer帧率丈量
了解RenderThread之前,咱们不得不提到Choreographer的烘托流程。
Choreographer烘托流程
-
主线程处于 Sleep 状况,等候 Vsync 信号
-
Vsync 信号到来,主线程被唤醒,Choreographer 回调 FrameDisplayEventReceiver.onVsync 开端一帧的制作
-
处理 App 这一帧的 Input 事情(假如有的话)
-
处理 App 这一帧的 Animation 事情(假如有的话)
-
处理 App 这一帧的 Traversal 事情(假如有的话)
-
主线程与烘托线程同步烘托数据,同步完毕后,主线程完毕一帧的制作,能够持续处理下一个 Message(假如有的话,IdleHandler 假如不为空,这时分也会触发处理),或许进入 Sleep 状况等候下一个 Vsync。
-
烘托线程首要需求从 BufferQueue 里边取一个 Buffer(dequeueBuffer) ,进行数据处理之后,调用 OpenGL 相关的函数,真正地进行烘托操作,然后将烘托好的 Buffer 还给 BufferQueue (queueBuffer) ,SurfaceFlinger 在 Vsync-SF 到了之后,将一切预备好的 Buffer 取出进行组成,如下图:
图片来历: 高爷: 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);
}
}
}
计划三: 字节码插桩
选用办法插桩监听每个办法的耗时,经过设置Looper的Printer,来监听监听每个Message的耗时,超越阀值,触发办法上报。
经过署理编译期间的使命 transformClassesWithDexTask,将全局 class 文件作为输入,运用 ASM 东西,高效地对一切 class 文件进行扫描及插桩。
修正字节码的办法,在编译期修正一切 class 文件中的函数字节码,对一切函数前后进行打点插桩。不过,有四点需求留意一下:
-
挑选在该编译使命履行时插桩,是由于 proguard 操作是在该使命之前就完结的,意味着插桩时的 class 文件现已被混淆过的。
-
挑选 proguard 之后去插桩,是由于假如提早插桩会形成部分办法不符合内联规则,没法在 proguard 时进行优化,终究导致程序办法数无法削减,然后引发办法数过大问题。
- 为了削减插桩量及功能损耗,经过遍历 class 办法指令集,判别扫描的函数是否只含有 PUT/READ FIELD 等简略的指令,来过滤一些默许或匿名构造函数,以及 get/set 等简略不耗时函数。
- 为了便利及高效记载函数履行进程,咱们为每个插桩的函数分配一个独立 ID,在插桩进程中,记载插桩的函数签名及分配的 ID,在插桩完结后输出一份 mapping,作为数据上报后的解析支撑。
六、 剖析东西
东西比照
运用指南
- 需求剖析Native代码,选Simpleperf
- 需求剖析体系调用,选Systrace
- 需求剖析运用流程和耗时,选TraceView / 插桩之后的Systrace
- 需求剖析其他运用,选Nanoscope
- 灰度环境剖析Java代码,选Rhea
七、 卡顿目标
怎样用一个目标直观反映卡顿呢?
监测目标
流通Smoothness核算
-
当Vsync信号抵达时会会调用HAL层的HWComposer.vsync()函数,告诉HWComposer引擎进行GPU烘托和显现,,然后发送Vsync信号给SurfaceFinger处理
-
SurfaceFinger接收到Vsync信号后,调用SurfaceFlinger的addResyncSample函数用来处理 Vsync 信号,addResyncSample函数能够将App的烘托帧同步到显现器的改写时刻,以防止呈现撕裂和卡顿等问题。
-
EventThread经过onVsyncEvent函数将Vsync信号分发给需求运用Vsync信号的App,完成更平滑和流通的烘托效果
-
当体系收到显现器的 Vsync 信号时,DisplayEventReceiver.onVsync() 函数会被调用,并将时刻戳和Displayer物理特色传递给App
-
App收到时刻戳和Displayer物理特色后,FrameHandler能够协助运用程序将烘托帧与 Vsync 信号同步,当 FrameHandler 接收到 Vsync 信号时,FrameHandler会调用 sendMessage() 办法,并将帧同步音讯作为参数传递给该办法
流通度存在哪些痛点问题?
流通度存在两个痛点:
第一个痛点是: 数据量大,不便利核算,导致淹没实在卡顿Case。
第二个痛点是: 不能简略运用平均值和方差。由于不同设备的卡顿规范线不一样,咱们应该按照设备等级划分标注线
流通度目标是怎样衡量的?
-
目标一: 流通度评分
- 压缩数据
- 加权放大卡顿
-
目标二: XPM评分(denzelzhou):离散程度
- 帧制作时长到规范制作时长的间隔(点到线的间隔)
- 间隔规范制作时长越远就越卡顿
八、监测SOP
监测规模
慢函数监测
技能需求: 经过外部装备阈值记载Android慢函数,假如超越阈值,那么将慢函数办法名和耗时刻信息记载在本地JSON文件
思路:
- 首要界说一个类 SlowFunctionClassVisitor,承继自 ClassVisitor,用于完成对类的字节码的修正。
- 在 SlowFunctionClassVisitor 中重写 visitMethod 办法,用于完成对办法的字节码的修正。在 visitMethod 办法中,先调用父类的 visitMethod 办法,然后运用 ASM 的 API 生成新的办法字节码,并将原来的办法字节码替换为新的办法字节码。
- 在生成新的办法字节码时,运用 Label 和 JumpInsnNode 等 ASM 的 API 刺进代码,完成对办法的耗时进行判别。假如办法耗时超越阈值,则记载慢函数信息,并将其写入本地 JSON 文件中。
- 为了完成记载慢函数信息和将其写入本地 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监测
咱们需求搜集的卡顿数据有: 卡顿次数/交互次数比,帧制作时刻采样. 处理办法能够参阅下面的内容:
- 界说一个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;
}
}
- 运用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();
}
}
}
}
}
- 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获取仓库信息有如下两大事务痛点:
-
功能损耗
-
需求暂停主线程运转
为了处理上述事务痛点,咱们能够采取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(false,true)) {
// 在 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…