/   今日科技快讯   /

近日,新华社发文评价盲盒经济称,盲盒不仅成为一个经济现象,也反映了当下中国年轻人,特别是“95后”一代的心理和生活状态。惊喜和期待的背后,“盲盒热”所带来的上瘾和赌博心理也在滋生畸形消费,不少盲盒爱好者每月花费不菲,正所谓“一入盲盒深似海,从此钱包是路人”。

/   作者简介   /

新的一周开始了,元旦也越来越近了!

本篇文章来自DoneWillianm的投稿,分享了他对卡顿工具的调研,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

DoneWillianm的博客地址:

https://www.jianshu.com/u/84e0c174a739

/   基础概念   /

这里主要是根据张绍文老师的文章做的笔记,根据张绍文老师的文笔去实践具体卡顿监控的内容

散列知识点

JVM中的线程切换大概花费CPU 20000个时钟周期

  • CPU

这里CPU需要单独搞出来提一下,卡顿优化前需要搞清楚CPU是什么,能干什么,正在干什么,然后才是“什么”这个区间里面应用程序此时的参数是否合理,优化的空间又是多少

查看一个CPU的参数需要看CPU的频率,核心等参数

这里就仅仅点相对重要的一些参数含义

  • 时钟周期:CPU每秒可以完成几个时钟周期,如

    可以完成这么多个时钟周期

  • 机器周期:主存中读取一个指令字的最短时间(由于CPU内部的操作速度较快.而CPU访问一次主存所花的时间较长,因此机器周期通常用主存中读取一个指令字的最短时间来规定。),所以 机器周期 = 时钟周期 * n(n >= 1)

  • 指令周期:完成一个指令需要的时间,一般由 几个或者一个机器周期组成,相当于 指令周期 = 机器周期 * n(n >= 1)

方法论-指标

  • CPU使用率

如果 CPU 使用率长期大于 60% ,表示系统处于繁忙状态,就需要进一步分析用户时间和系统时间的比例。对于普通应用程序,系统时间不会长期高于 30%,如果超过这个值,就得考虑是否I/O调用过多或者锁调用的过于频繁的问题。利用Android Studio的profile也能查看CPU的使用率

  • CPU饱和度

CPU 饱和度反映的是线程排队等待 CPU 的情况,也就是 CPU 的负载情况。

CPU 饱和度首先会跟应用的线程数有关,如果启动的线程过多,易导致系统不断地切换执行的线程,把大量的时间浪费在上下文切换,要知道每一次 CPU 上下文切换都需要刷新寄存器和计数器,至少需要几十纳秒的时间。

可以通过vmstat命令查看CPU上下文切换次数

proc/self/sched:nr_voluntary_switches:主动上下文切换次数,因为线程无法获取资源导致上下文切换,最普遍的就是IOnr_involuntary_switches:被动上下文切换次数,线程被系统强制调度导致上下文切换,例如大量线程在抢占CPUse.statistics.iowait_count:IO 等待次数se.statistics.iowait_sum:IO 等待时间

此外也可以通过 uptime 命令可以检查 CPU 在 1 分钟、5 分钟和 15 分钟内的平均负载。比如一个 4 核的 CPU,如果当前平均负载是 8,这意味着每个 CPU 上有一个线程在运行,还有一个线程在等待。一般平均负载建议控制在“0.7 × 核数”以内。

00:02:39 up 7 days, 46 min,  0 users,
load average: 13.91, 14.70, 14.32

另外一个会影响 CPU 饱和度的是线程优先级,线程优先级会影响 Android 系统的调度策略,它主要由 nice 和 cgroup 类型共同决定。nice 值越低,抢占 CPU 时间片的能力越强。当 CPU 空闲时,线程的优先级对执行效率的影响并不会特别明显,但在 CPU 繁忙的时候,线程调度会对执行效率有非常大的影响。

关于线程优先级,你需要注意是否存在高优先级的线程空等低优先级线程,例如主线程等待某个后台线程的锁。从应用程序的角度来看,无论是用户时间、系统时间,还是等待 CPU 的调度,都是程序运行花费的时间。

/   市场调研   /

Traceview 和 systrace 都是我们比较熟悉的排查卡顿的工具,从实现上这些工具分为两个流派。

第一个流派是 instrument。获取一段时间内所有函数的调用过程,可以通过分析这段时间内的函数调用流程,再进一步分析待优化的点。

第二个流派是 sample。有选择性或者采用抽样的方式观察某些函数调用过程,可以通过这些有限的信息推测出流程中的可疑点,然后再继续细化分析。

根据流派,对目前市场上的性能监控工具做一些调研和使用,包括但不限于官方提供的性能监控工具,如systrace,Matrix等,关于Android上Systrace的使用可以参考我之前写过的一个blog里面有提到过如何使用Android 性能优化(https://www.jianshu.com/p/de82190d6bb1)

选择哪种工具,需要看具体的场景。如果需要分析 Native 代码的耗时,可以选择 Simpleperf;如果想分析系统调用,可以选择 systrace;如果想分析整个程序执行流程的耗时,可以选择 Traceview 或者插桩版本的 systrace。

对目前市场上的一些性能监控框架做基本调研,如DoraemonKit,Matrix,BlockCanary

BlockCanary & DoraemonKit

这里之所以把两个都放到一起,是因为滴滴的哆啦A梦的卡顿检测其实就是blockCanary,实现很简单,但是思路很巧妙~

想要检测卡顿,其实就是检测主线程的运行情况,为什么这么说呢,因为每一帧渲染数据的创建,就依托于主线程来创建,而想要保证每一帧CPU都能在16.7ms内(这里仅限于60帧这种情况,如果是90或者120,可以反推的哈~)完成工作,这样就不会出现丢帧的现象,也就不会造成卡顿。

而如何监测主线程的运行情况呢?这里需要知道安卓中的handler机制,通过检测每次处理主线程消息的耗时情况,就能够知道是否产生了卡顿,而在发生卡顿的时候,同时抓取此时主线程的堆栈,那么就更能方便的定位到需要优化的代码。

BlockCanary核心的地方,主要分为两个部分:

  1. 检测handleMessage

  2. 主线程抓取堆栈的部分

  • handleMessage

在主线程Looper每次处理消息的过程中,通过hook主线程Looper每次处理消息的过程,在处理消息之前记录一个时间戳,处理完消息之后记录一个时间戳,那么两个时间的差值,就是处理一条消息所花费的时间。通过给这个时间设置阈值,如:处理时间 > 阈值时间(430ms > 200ms)那么就认为是发生了卡顿

这里的hook其实非常简单,因为framework给咱们预留了这样的口子,可以看下在handlemessage这里的源码:

public void loop(){for (;;) {Message msg = queue.next(); // might blockif (msg == null) {// No message indicates that the message queue is quitting.return;}// This must be in a local variable, in case a UI event sets the loggerfinal Printer logging = me.mLogging;if (logging != null) {logging.println(">>>>> Dispatching to " + msg.target + " " +msg.callback + ": " + msg.what);}......final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;final long dispatchEnd;try {msg.target.dispatchMessage(msg);dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;} finally {if (traceTag != 0) {Trace.traceEnd(traceTag);}}if (logging != null) {logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);}msg.recycleUnchecked();}
}....../*** 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, identifying the* target Handler and message contents.** @param printer A Printer object that will receive log messages, or* null to disable message logging.*/
public void setMessageLogging(@Nullable Printer printer) {mLogging = printer;
}

可见们只需要手动调用Looper.setMessageLogging方法就能给线程looper对象设置printer对象,在每次处理消息的时候,通过监听printer的println回调,解析出内容,就能知道知道分发消息的开始和结束了

//Printer 接口
public interface Printer {/*** Write a line of text to the output.  There is no need to terminate* the given string with a newline.*/void println(String x);
}

通过过滤printer.println打印的内容,判断是否是在消息分发处理相关的内容,然后进行时间差的计算,来判断卡顿是否发生

//LooperPrinter
class LooperPrinter implements Printer {public Printer origin;boolean isHasChecked = false;boolean isValid = false;LooperPrinter(Printer printer) {this.origin = printer;}@Overridepublic void println(String x) {if (null != origin) {origin.println(x);if (origin == this) {throw new RuntimeException(MonitorConstants.LOG_TAG + " origin == this");}}if (!isHasChecked) {isValid = x.charAt(0) == '>' || x.charAt(0) == '<';isHasChecked = true;if (!isValid) {InsectLogger.ne("[println] Printer is inValid! x:%s", x);}}if (isValid) {dispatch(x.charAt(0) == '>', x);}}
}

那么依据主线程卡顿的监控就已经完成了,接下来是对于卡顿问题的定位,也就是对主线程堆栈的抓取

dumpStack

这里不完全参照blockCanary的实现,但是大家都是为了解决能够抓取到问题发生的堆栈,这里先说一下对于主线程堆栈dump需要关注的问题。不同的抓取策略也是为了解决这个问题,此处先不考虑对性能带来的影响

假设此时发生了卡顿,那么在调用getStackTrace的时候,这时候虚拟机中所跟踪的堆栈中会把当前记录的一些堆栈返回。通过在发生卡顿的时候,dump出当前的堆栈,记录下来,再追溯问题的时候直接看存储下来的堆栈信息,那么定位问题就会方便很多

而实际情况下并不能如此理想,因为从VM中取出的堆栈dalvik.system.VMStack#getThreadStackTrace返回的数据是未知的,不能保证里面到底有多少内容,可能只有一部分,这样就可能会遗漏真正的问题所在,可以参考下图~

可以看到真正有问题的函数其实是FunctionA-1,而如果捞出来的堆栈只有FunctionA-2或者A-3的话,当然可以优化A-3,但是会漏掉真正发生问题的函数。

所以对于堆栈的抓取,基于VMStack抓取堆栈的方式下笔者思考了两种方案来解决这样的问题,这两种应该也是市面上基于VMStack方式的大概方案,再深入往VM中去研究感觉可以有,但是不推荐,因为成本高,且回报的话不太会有预期中的高。

  • 周期性Dump

通过每个一段时间从VM中获取主线程的堆栈,在发生卡顿的时候,过滤出时间,然后直接取出这段时间内的堆栈来进行问题排查。

在实现的时候需要注意的一些小细节:

  1. 循环队列

  2. 堆栈去重

  3. 时间区间筛选

  • 起止Dump

这里可以“忽略”多线程的特性,因为我们关注的仅仅是主线程,那么只需要在消息分发之初dump一次堆栈,然后再消息处理之后再dump一次堆栈,这样既能在dump出来的堆栈中发现可能存在的问题,同时又能自行推断这中间的执行过程来观测代码中出现的问题。

当然不可缺少一个代码耗时检测的小工具(https://github.com/DoneMr/ASMApp)

Matrix

关于matrix-traceCanary原理(https://github.com/Tencent/matrix/wiki/Matrix-Android-TraceCanary)

关于matrix解剖,需要先了解定义,再根据具体代码进行分析,最后根据代码梳理出实现的思路

  • 卡顿定义

微信开发者对于卡顿的定义,很简单,很清晰,很明了,这里就cv过来了,一定要仔细读对卡顿的定义

什么是卡顿,很多人能马上联系到的是帧率 FPS (每秒显示帧数)。那么多低的 FPS 才是卡顿呢?又或者低 FPS 真的就是卡顿吗?(以下 FPS 默认指平均帧率)

其实并非如此,举个例子,游戏玩家通常追求更流畅的游戏画面体验一般要达到 60FPS 以上,但我们平时看到的大部分电影或视频 FPS 其实不高,一般只有 25FPS ~ 30FPS,而实际上我们也没有觉得卡顿。

在人眼结构上看,当一组动作在 1 秒内有 12 次变化(即 12FPS),我们会认为这组动作是连贯的;而当大于 60FPS 时,人眼很难区分出来明显的变化,所以 60FPS 也一直作为业界衡量一个界面流畅程度的重要指标。一个稳定在 30FPS 的动画,我们不会认为是卡顿的,但一旦 FPS 很不稳定,人眼往往容易感知到。

FPS 低并不意味着卡顿发生,而卡顿发生 FPS 一定不高。FPS 可以衡量一个界面的流程性,但往往不能很直观的衡量卡顿的发生,这里有另一个指标(掉帧程度)可以更直观地衡量卡顿。

什么是掉帧(跳帧)?按照理想帧率 60FPS 这个指标,计算出平均每一帧的准备时间有 1000ms/60 = 16.6667ms,如果一帧的准备时间超出这个值,则认为发生掉帧,超出的时间越长,掉帧程度越严重。

假设每帧准备时间约 32ms,每次只掉一帧,那么 1 秒内实际只刷新 30 帧,即平均帧率只有 30FPS,但这时往往不会觉得是卡顿。反而如果出现某次严重掉帧(>300ms),那么这一次的变化,通常很容易感知到。所以界面的掉帧程度,往往可以更直观的反映出卡顿。

  • 流畅性

综上所述,其实可以明白对于卡顿的定义,衡量流畅性的指标可以简单理解为:

在用户有操作的前提下平均掉帧率。只有在某一时刻发生的掉帧情况远远大于其他时刻,那么才界定为卡顿,这也是上面说到的界面的掉帧程度,才更直观的反映出卡顿

  • Code实现

关于反射 Choreographer 来做到如何监测用户触发后开始计算平均帧率。关于Choreographer的知识相关(https://juejin.cn/post/6863756420380196877)

这里不做赘述,只根据实现原理来对使用的地方做说明

1. 用户触发刷新

了解下源码中callbackType的含义

    /*** Callback type: Input callback.  Runs first.* @hide*/public static final int CALLBACK_INPUT = 0;/*** Callback type: Animation callback.  Runs before traversals.* @hide*/@TestApipublic static final int CALLBACK_ANIMATION = 1;/*** Callback type: Traversal callback.  Handles layout and draw.  Runs* after all other asynchronous messages have been handled.* @hide*/public static final int CALLBACK_TRAVERSAL = 2;

显而易见,CALLBACK_INPUT都已经注释好了,首先run的是这个类型的回调,然后我们平时注册的又是什么样子呢?

/*** Posts a frame callback to run on the next frame.* <p>* The callback runs once then is automatically removed.* </p>** @param callback The frame callback to run during the next frame.** @see #postFrameCallbackDelayed* @see #removeFrameCallback*/
public void postFrameCallback(FrameCallback callback) {postFrameCallbackDelayed(callback, 0);
}/*** Posts a frame callback to run on the next frame after the specified delay.* <p>* The callback runs once then is automatically removed.* </p>** @param callback The frame callback to run during the next frame.* @param delayMillis The delay time in milliseconds.** @see #postFrameCallback* @see #removeFrameCallback*/
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {if (callback == null) {throw new IllegalArgumentException("callback must not be null");}postCallbackDelayedInternal(CALLBACK_ANIMATION,callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

注册类型的callbackType为ANIMATION的,而ANIMATION的type又是什么时候回调呢?

void doFrame(long frameTimeNanos, int frame) {final long startNanos;synchronized (mLock) {......try {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);//优先执行CALLBACK_INPUT类型链表里面的回调mFrameInfo.markInputHandlingStart();doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);mFrameInfo.markAnimationsStart();doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);mFrameInfo.markPerformTraversalsStart();doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);} finally {AnimationUtils.unlockAnimationClock();Trace.traceEnd(Trace.TRACE_TAG_VIEW);}......
}

可见是在执行完优先级最高的输入类型的回调才会回调ANIMATION的(注意这里提供的两个参数,第一个是执行该frame的时间戳,第二个是当前帧号,是native调用,在DisplayEventReceiver事件中收到后维护的一个成员变量,具体实现类也在Choreagrapher中),而显然不能够符合我们的要求,我们是期望在用户有操作的情况下是否发生丢帧情况

而如何计算input时机的帧率呢?势必需要在input类型中添加自己实现的callback,在animation开始的执行的时候,标识为input执行结束

好了,原理分析完毕,接下来看一下在Matrix中带佬如何实现的,核心类主要是com.tencent.matrix.trace.core.UIThreadMonitor

初始化中先拿到需要hook的方法,然后模拟顺序进行执行

public void init(TraceConfig config) {......//反射同步锁对象和对应doFrame的所有回调数组链表对象choreographer = Choreographer.getInstance();callbackQueueLock = reflectObject(choreographer, "mLock");callbackQueues = reflectObject(choreographer, "mCallbackQueues");//先拿到添加对应回调的可执行反射方法addInputQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class);addAnimationQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class);addTraversalQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class);frameIntervalNanos = reflectObject(choreographer, "mFrameIntervalNanos");......this.isInit = true;......
}

通过上面分析可得,doframe的回调执行是顺序执行下来,也就是说一个类型的callback执行结束时间,就是下一个类型的开始时间,那么在addCallback的时机也是如此,最开始要添加的则是input类型回调

public void init(TraceConfig config) {......choreographer = Choreographer.getInstance();callbackQueueLock = ReflectUtils.reflectObject(choreographer, "mLock", new Object());//反射获取回调的数组链表对象,可以理解为单object的hashMap  数组+链表实现callbackQueues = ReflectUtils.reflectObject(choreographer, "mCallbackQueues", null);if (null != callbackQueues) {addInputQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class);addAnimationQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class);addTraversalQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class);}//主要是拿vsync回调上来的信号开始绘制的时间戳,可以分析出来丢帧数,源码也是这么干的vsyncReceiver = ReflectUtils.reflectObject(choreographer, "mDisplayEventReceiver", null);//产生vsync信号的时间戳frameIntervalNanos = ReflectUtils.reflectObject(choreographer, "mFrameIntervalNanos", Constants.DEFAULT_FRAME_DURATION);LooperMonitor.register(new LooperMonitor.LooperDispatchListener() {@Overridepublic boolean isValid() {return isAlive;}@Overridepublic void dispatchStart() {super.dispatchStart();UIThreadMonitor.this.dispatchBegin();}@Overridepublic void dispatchEnd() {super.dispatchEnd();UIThreadMonitor.this.dispatchEnd();}});......
}public synchronized void onStart() {......if (!isAlive) {this.isAlive = true;synchronized (this) {MatrixLog.i(TAG, "[onStart] callbackExist:%s %s", Arrays.toString(callbackExist), Utils.getStack());callbackExist = new boolean[CALLBACK_LAST + 1];}//为三种callback增加状态维护数组queueStatus = new int[CALLBACK_LAST + 1];//为三种callback增加耗时数组queueCost = new long[CALLBACK_LAST + 1];//首次添加input类型callbackaddFrameCallback(CALLBACK_INPUT, this, true);}
}

可以看到,在添加input类型的回调时,传的是自己,那么来分析一下接下来的run实现

@Override
public void run() {//来自vsync信号开始doFrameBegin(token);//维护input类型数组们的状态doQueueBegin(CALLBACK_INPUT);//animation回调注册回调addFrameCallback(CALLBACK_ANIMATION, new Runnable() {@Overridepublic void run() {//animation回调,input结束doQueueEnd(CALLBACK_INPUT);doQueueBegin(CALLBACK_ANIMATION);}}, true);addFrameCallback(CALLBACK_TRAVERSAL, new Runnable() {@Overridepublic void run() {//traversal类型回调,animation结束doQueueEnd(CALLBACK_ANIMATION);doQueueBegin(CALLBACK_TRAVERSAL);}}, true);
}

可以看到,如此就能得到一个Vsync信号过来的轮回,但是走到这里只能完成一次,matrix如何把每一次串起来的咧?

还记得上面初始化的时候注册looper监听,每次消息的处理开始和结束都会激活一次dispatchStart和dispatchEnd,start这里就不分析了,其实就是往外回调,主要是end

private void dispatchEnd() {......//在第一次Vsync开始的时候赋值为true,直接进来if (isVsyncFrame) {doFrameEnd(token);intendedFrameTimeNs = getIntendedFrameTimeNs(startNs);}......//来自一次vsync信号结束this.isVsyncFrame = false;
}private void doFrameEnd(long token) {//下一次input开始,上一次的traversal结束doQueueEnd(CALLBACK_TRAVERSAL);......//开启下一次input轮回addFrameCallback(CALLBACK_INPUT, this, true);
}

可以看到,这里才是真正的结束,一个完整的Choreographer循环~

卡顿策略

Matrix的文档里面已经非常清楚的用文字描述一个卡顿是如何产生的,以及卡顿的定义

然后分析下关于瞬时平均帧率的代码需要重点关注的就是com.tencent.matrix.trace.tracer.FrameTracer这个类

在每一次doFrame的回调中去分析这个参数

@Override
public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {if (isForeground()) {notifyListener(focusedActivity, startNs, endNs, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);}
}private void notifyListener(final String focusedActivity, final long startNs, final long endNs, final boolean isVsyncFrame,final long intendedFrameTimeNs, final long inputCostNs, final long animationCostNs, final long traversalCostNs) {try {//计算丢帧,跟源码的计算方式一致,上一个版本局部变量的声明并没有如此直观,这个版本改了,清爽许多final long jiter = endNs - intendedFrameTimeNs;final int dropFrame = (int) (jiter / frameIntervalNs);synchronized (listeners) {//listeners目前注册进来的就俩,一个内部类FPSCollect,一个用于UI展示的FrameDecoratorfor (final IDoFrameListener listener : listeners) {if (config.isDevEnv()) {listener.time = SystemClock.uptimeMillis();}if (null != listener.getExecutor()) {if (listener.getIntervalFrameReplay() > 0) {//数据收集部分listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);} else {//卡顿分析部分listener.getExecutor().execute(new Runnable() {@Overridepublic void run() {listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);}});}} else {listener.doFrameSync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);}}}} finally {}
}

为什么说丢帧计算和源码一致呢?这里我们可以和源码对比一下:

final long jiter = endNs - intendedFrameTimeNs;
final int dropFrame = (int) (jiter / frameIntervalNs);//源码 android.view.Choreographer#doFrame
final long jitterNanos = startNanos - frameTimeNanos;
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.");
}

这么看上去,瞬间就友好了很多~

从注释里面也能看到,注册的俩监听,一个用于记录,一个用于展示,记录其实就是填充此时此刻的关于FPS的快照,没什么可看的,学习而言,展示的要好一些,因为他需要分析数据,然后展示到UI上.

那么接下来就直接看下Matix中的com.tencent.matrix.trace.view.FrameDecorator#doFrameAsync,源码过长,这里就一步一步分析代码是如何体现上面的表和描述

final long jiter = endNs - intendedFrameTimeNs;
/*** 流畅指标,佳0,正常1,中等2,严重3,冻帧4*/
public enum DropStatus {DROPPED_FROZEN(4), DROPPED_HIGH(3), DROPPED_MIDDLE(2), DROPPED_NORMAL(1), DROPPED_BEST(0);public int index;DropStatus(int index) {this.index = index;}
}

在回调回来的函数中,分析流畅指标

@Override
public void doFrameAsync(String focusedActivity, long startNs, long endNs, int dropFrame, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {super.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);......if (dropFrame >= Constants.DEFAULT_DROPPED_FROZEN) { //冻帧dropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]++;sumDropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]++;} else if (dropFrame >= Constants.DEFAULT_DROPPED_HIGH) { //严重dropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index]++;sumDropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index]++;} else if (dropFrame >= Constants.DEFAULT_DROPPED_MIDDLE) { //中等dropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index]++;sumDropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index]++;} else if (dropFrame >= Constants.DEFAULT_DROPPED_NORMAL) { //正常dropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index]++;sumDropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index]++;} else {dropLevel[FrameTracer.DropStatus.DROPPED_BEST.index]++;sumDropLevel[FrameTracer.DropStatus.DROPPED_BEST.index]++;}......
}

这里的代码非常简单,接下来是分析造成严重卡顿的情况,也就是严重丢帧的时候,也是文章中所分析的内容:

如果出现某次严重掉帧(>300ms),那么这一次的变化,通常很容易感知到

sumFrameCost += (dropFrame + 1) * frameIntervalMs;
sumFrames += 1;
float duration = sumFrameCost - lastCost[0];
long collectFrame = sumFrames - lastFrames[0];
if (duration >= 200) {//更新视图
}

综上,就是Matrix中对页面流畅性分析的核心代码,而对于Matrix中精准命中堆栈则可以取自冻帧或者所谓的duration >= 200这个条件下dump一次主线程的堆栈来获取

慢函数

其实关于上述严重掉帧情况下的抓取堆栈的数量不多,同样避不开上面提到的漏掉其他耗时代码的情况,不过笔者认为这样的情况不会特别多,因为卡顿发生的时候,大概率避免不了正在执行一个耗时操作,那么这个耗时操作的堆栈出现在此刻dump出来的堆栈里面的可能性很大,所以Matrix干脆利落的出了一个慢函数检测。

这样感觉就无孔不入了,对所有在主线程上运行的函数耗时进行收集,和BlockCanary检测卡顿的策略一样,但是堆栈的抓取就要复杂一些,这也是为什么Matrix性能更好的原因,能在灰度下上线的能力。同时也能有对应的聚合策略,这样结合后端的能力方便我们分析代码运行情况,然后做 “狭义” 上的优化。

这里推荐一个在解决卡顿时候您可以用到的方法耗时小插件(https://www.jianshu.com/p/c6d8aa2671ff)

Matrix中的慢函数和BlockCanary的卡顿堆栈获取时机和检测卡顿或者慢的策略一致,下面可以简单看下,关于Matrix中如何去捞堆栈的~

@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 {//查过阈值,和BlockCanary一样com.tencent.matrix.trace.constants.Constants.DEFAULT_EVIL_METHOD_THRESHOLD_MS = 700if (dispatchCost >= evilThresholdMs) {long[] data = AppMethodBeat.getInstance().copyData(indexRecord);long[] queueCosts = new long[3];System.arraycopy(queueTypeCosts, 0, queueCosts, 0, 3);//scene拿的是当前的activityString scene = AppMethodBeat.getVisibleScene();MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, queueCosts, cpuEndMs - cpuBeginMs, dispatchCost, endNs / Constants.TIME_MILLIS_TO_NANO));}} finally {indexRecord.release();}
}

可以看到,慢函数的阈值是700ms,如果超出阈值,则会从AppMethod中拿取相关的堆栈数据,同时记录下当前的页面然后上传一波,那么关键的地方就在于这个

AppMethodBeat.getInstance().copyData(indexRecord)

关于Matrix的堆栈实现主要分为两块

  1. ASM插桩记录方法

  2. Java侧关于记录方法耗时以及方法记录的实现

编译期:

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

插桩过程有几个关键点:

1、选择在该编译任务执行时插桩,是因为 proguard 操作是在该任务之前就完成的,意味着插桩时的 class 文件已经被混淆过的。而选择 proguard 之后去插桩,是因为如果提前插桩会造成部分方法不符合内联规则,没法在 proguard 时进行优化,最终导致程序方法数无法减少,从而引发方法数过大问题。

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

3、针对界面启动耗时,因为要统计从 Activity#onCreate 到 Activity#onWindowFocusChange 间的耗时,所以在插桩过程中需要收集应用内所有 Activity 的实现类,并覆盖 onWindowFocusChange 函数进行打点。

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

下面重点介绍一下关于Matrix中对性能考量以及具体的业务插桩代码

编译优化,内联规则

选择在该编译任务执行时插桩,是因为 proguard 操作是在该任务之前就完成的,意味着插桩时的 class 文件已经被混淆过的。而选择 proguard 之后去插桩,是因为如果提前插桩会造成部分方法不符合内联规则,没法在 proguard 时进行优化,最终导致程序方法数无法减少,从而引发方法数过大问题。

解释一下什么叫做方法内联,说白了其实就是你写了两个方法,在编译时会将其中一个方法的实现直接放到调用的地方,这样就无需去走一遍调用其他方法,从而达到优化的目的,因为每个方法会生成一个栈帧,然后进行压栈出栈的操作,过程比较反复,这里用代码解释一下编译优化之一,方法内联:

//编译前,source.java
public int doubleNum(int num){return num * 2;
}public void method1(){int a = 10;int b = doubleNum(2);System.out.println(String.format("a:%s, b:%s", a, b));
}//编译后, source.class
public void method1(){int a = 10;int b = a * 2;//doubleNum(2);System.out.println(String.format("a:%s, b:%s", a, b));
}

可以看到在编译之后就没有了doubleNum这个方法,在method1中调用的时候直接变成了 a * 2 , 这样就无需在运行的过程中去对doubleNum压栈操作

这里有一篇介绍jvm中关于内联的blog(https://blog.csdn.net/riemann_/article/details/104114775)

虽然能保证proguard时候优化能正常进行,不过优化程度上来讲,笔者也没有统计过哈,也不晓得从哪里能够看到,因为目前在7.0之后,安装的时候会进行JIT和AOT混合的方式,这个结果应该不是很好看,在JIT选择编译热代码的时候,优化的那部分内联又该如何考虑呢?想想太复杂了。。。大家感兴趣的话可以统计下在打出来的release包中,可以看日志,优化的效果~

那接下里就是Matrix中如何做到在proguard的transform后进行插桩呢,核心工程是matrix-gradle-plugin(https://github.com/Tencent/matrix/tree/master/matrix/matrix-android/matrix-gradle-plugin)

入口:

//com.tencent.matrix.plugin.MatrixPlugin#apply/*** <p>Adds a closure to be called immediately after this project has been evaluated. The project is passed to the* closure as a parameter. Such a listener gets notified when the build file belonging to this project has been* executed. A parent project may for example add such a listener to its child project. Such a listener can further* configure those child projects based on the state of the child projects after their build files have been* run.</p>** @param closure The closure to call.*/
void afterEvaluate(Closure closure);@Override
void apply(Project project) {......//完成所有transform之后执行project.afterEvaluate {def android = project.extensions.androiddef configuration = project.matrixandroid.applicationVariants.all { variant ->if (configuration.trace.enable) {//代码插桩入口MatrixTraceTransform.inject(project, configuration.trace, variant.getVariantData().getScope())}......}}
}

在afterEvaluate后传入闭包,开始插桩代码,为什么是这个时机,可以参考上面的源码注释哈~

接下来就是注入代码,因为不依赖于自定义的transformTask,Matrix的实现是通过往transformTask里面去注入执行事件,这里的写法也是个新姿势~

//com.tencent.matrix.trace.transform.MatrixTraceTransform#injectpublic static void inject(Project project, MatrixTraceExtension extension, VariantScope variantScope) {......try {String[] hardTask = getTransformTaskName(extension.getCustomDexTransformName(), variant.getName());for (Task task : project.getTasks()) {for (String str : hardTask) {if (task.getName().equalsIgnoreCase(str) && task instanceof TransformTask) {//如果确实是Transform的task后进来执行反射hook注入matrix的transform任务TransformTask transformTask = (TransformTask) task;Log.i(TAG, "successfully inject task:" + transformTask.getName());Field field = TransformTask.class.getDeclaredField("transform");field.setAccessible(true);//这里就是注入自定义的任务,在编译执行的时候field.set(task, new MatrixTraceTransform(config, transformTask.getTransform()));break;}}}} catch (Exception e) {Log.e(TAG, e.toString());}}//源码中可以看到执行transformTask的时候调用这个注入的transform执行处,com.android.build.gradle.internal.pipeline.TransformTask#transform
@TaskAction
void transform(final IncrementalTaskInputs incrementalTaskInputs)throws IOException, TransformException, InterruptedException {......ThreadRecorder.get().record(ExecutionType.TASK_TRANSFORM,new Recorder.Block<Void>() {@Overridepublic Void call() throws Exception {//这里就是调用transform.transform的时机transform.transform(new TransformInvocationBuilder(TransformTask.this).addInputs(consumedInputs.getValue()).addReferencedInputs(referencedInputs.getValue()).addSecondaryInputs(changedSecondaryInputs.getValue()).addOutputProvider(outputStream != null? outputStream.asOutput(): null).setIncrementalMode(isIncremental.getValue()).build());return null;}},new Recorder.Property("project", getProject().getName()),new Recorder.Property("transform", transform.getName()),new Recorder.Property("incremental", Boolean.toString(transform.isIncremental())));
}

[手动表情秒啊~],简直了。。。 不愧是微信的带佬,如果不是对编译任务的task有一定了解的话,要做到这里感觉不太可能。。。 不过还好能站在巨人的肩膀上~

这里注意下关于MatrixTraceTransform的构造,这里也是一个优秀的细节处理,虽然hook了系统transform的task,但是不会扔弃系统的transformTask,而是会传递进来,接下来在代码中分析这个伪代理的使用~

@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {super.transform(transformInvocation);long start = System.currentTimeMillis();try {//在执行系统的transform之前,执行自己的transformdoTransform(transformInvocation); // hack} catch (ExecutionException e) {e.printStackTrace();}long cost = System.currentTimeMillis() - start;long begin = System.currentTimeMillis();//ok 接下来,执行系统原来内置的orignTransformTask, 优秀的细节,不过貌似也只能这么干,可以考虑到卡顿hook looer中printer的时候也可以这么干~origTransform.transform(transformInvocation);
}

最后就是插桩的核心代码了,这里分为两个部分来进行介绍,一个是如何过滤简单方法,一个是插桩细节

  • 过滤简单方法

  • 插桩

总的来说,流程也比较简单,整个方法分为三个过程

首先解析mapping文件,毕竟是对混淆过的代码插桩,这里要能知道自己到底插的哪个方法

然后是收集要插桩的方法,就是过滤,过滤黑名单中不需要插桩的类或者方法。对收集后的方法开始插桩

精简一下代码:

//com.tencent.matrix.trace.transform.MatrixTraceTransform#doTransform
private void doTransform(TransformInvocation transformInvocation) throws ExecutionException, InterruptedException {final boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental();/*** step 1 把编译后的mapping文件对应的混淆方法关系记录下来,如:a() -> onCreate()*/List<Future> futures = new LinkedList<>();//...干掉乱八七糟的代码干掉哈,主要就是生成方法id,然后解析~//拿到需要查找的类文件for (TransformInput input : inputs) {for (DirectoryInput directoryInput : input.getDirectoryInputs()) {futures.add(executor.submit(new CollectDirectoryInputTask(dirInputOutMap, directoryInput, isIncremental)));}for (JarInput inputJar : input.getJarInputs()) {futures.add(executor.submit(new CollectJarInputTask(inputJar, isIncremental, jarInputOutMap, dirInputOutMap)));}}//这里调用get方法就是执行for (Future future : futures) {future.get();}futures.clear();/*** step 2 这里就是收集下来需要进行插桩的方法,过滤出来黑名单或者是简单方法,这些方法不需要插桩~  这里这个判断再一次[手动秒啊~]*/MethodCollector methodCollector = new MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap);methodCollector.collect(dirInputOutMap.keySet(), jarInputOutMap.keySet());/*** step 3 对收集的方法进行插桩~*/MethodTracer methodTracer = new MethodTracer(executor, mappingCollector, config, methodCollector.getCollectedMethodMap(), methodCollector.getCollectedClassExtendMap());methodTracer.trace(dirInputOutMap, jarInputOutMap);}

mapping的解析就是工作量问题,这里主要看分析函数是否简单的地方,先看一下文档中的定义

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

简单方法过滤

内部获取方法同样利用ASM那一套逻辑,上面介绍耗时插件已经介绍过了哈~可以重点关注核心代码:

//com.tencent.matrix.trace.MethodCollector.TraceClassAdapter#visitMethod
@Override
public MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions) {//抽象类不做处理if (isABSClass) {return super.visitMethod(access, name, desc, signature, exceptions);} else {//判断该方法是不是onWindowFocusif (!hasWindowFocusMethod) {hasWindowFocusMethod = isWindowFocusChangeMethod(name, desc);}//真正核心处理过滤逻辑return new CollectMethodNode(className, access, name, desc, signature, exceptions);}
}//com.tencent.matrix.trace.MethodCollector.CollectMethodNode#visitEnd
@Override
public void visitEnd() {super.visitEnd();TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);if ("<init>".equals(name)) {isConstructor = true;}//判断是否是黑名单里面的,这里和黑名单中进行了互斥,在插桩中也会判断//所以可以不用在意这个细节,说白了就是判断这个方法不在黑名单中而已boolean isNeedTrace = isNeedTrace(configuration, traceMethod.className, mappingCollector);// filter simple methods 这里就是过滤简单方法,我们重点关注这里if ((isEmptyMethod() || isGetSetMethod() || isSingleMethod())&& isNeedTrace) {ignoreCount.incrementAndGet();collectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);return;}......
}

上面截取出来的额低吗可见端倪,一个是判断构造方法,然后就是isEmptyMethod,isGetSetMethod,isSingleMethod,接下来就分析下这个所谓的过滤简单方法到底????????与否

//com.tencent.matrix.trace.MethodCollector.CollectMethodNode#isEmptyMethod
/*** 检测空方法,不知道这里为什么这么写。。。反正我验证这个方法基本没有用* -1 是F_NEW指令,不应该是判断return指令么?** @return*/
private boolean isEmptyMethod() {ListIterator<AbstractInsnNode> iterator = instructions.iterator();while (iterator.hasNext()) {//逻辑就是过滤掉是new指令?  说白了就是如果指令集不为空,就不是空方法AbstractInsnNode insnNode = iterator.next();int opcode = insnNode.getOpcode();//-1对应的opcode是NEW这个指令if (-1 == opcode) {continue;} else {return false;}}return true;
}/*这里是空方法反编译后的字节码public void logClueMethod();descriptor: ()Vflags: ACC_PUBLICCode:stack=0, locals=1, args_size=10: returnLineNumberTable:line 22: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       1     0  this   Lcom/done/testlibrary/Utils;*/

首先空方法的判断不够牛批哈~接下来看第二个isGetSetMethod

private boolean isGetSetMethod() {int ignoreCount = 0;ListIterator<AbstractInsnNode> iterator = instructions.iterator();while (iterator.hasNext()) {AbstractInsnNode insnNode = iterator.next();int opcode = insnNode.getOpcode();if (-1 == opcode) {continue;}if (opcode != Opcodes.GETFIELD&& opcode != Opcodes.GETSTATIC&& opcode != Opcodes.H_GETFIELD&& opcode != Opcodes.H_GETSTATIC&& opcode != Opcodes.RETURN&& opcode != Opcodes.ARETURN&& opcode != Opcodes.DRETURN&& opcode != Opcodes.FRETURN&& opcode != Opcodes.LRETURN&& opcode != Opcodes.IRETURN&& opcode != Opcodes.PUTFIELD&& opcode != Opcodes.PUTSTATIC&& opcode != Opcodes.H_PUTFIELD&& opcode != Opcodes.H_PUTSTATIC&& opcode > Opcodes.SALOAD) {if (isConstructor && opcode == Opcodes.INVOKESPECIAL) {ignoreCount++;if (ignoreCount > 1) {return false;}continue;}return false;}}return true;
}

最后是判断是不是简单方法

private boolean isSingleMethod() {ListIterator<AbstractInsnNode> iterator = instructions.iterator();while (iterator.hasNext()) {AbstractInsnNode insnNode = iterator.next();int opcode = insnNode.getOpcode();if (-1 == opcode) {continue;//出现这个指令区间内,都标识调用了其他方法,会出现压栈的情况// 调用了别的方法,自然就不是简单的方法,不过这里没有判断指令的数量,感觉也不是一定可靠} else if (Opcodes.INVOKEVIRTUAL <= opcode && opcode <= Opcodes.INVOKEDYNAMIC) {return false;}}return true;
}

三个过滤的函数都看完了,感觉不一定可取,可以借鉴和参考,但不一定准,可能是在下没有严谨的编译看吧。。。不过有这种思路也还好,可以自己定义所谓的简单方法吧~

插桩代码

这里只简单看一下插桩的代码,具体计算耗时的功能逻辑实现后面会继续介绍,那部分也不再plugin的工程中~

插桩对外的类是com.tencent.matrix.trace.MethodTracer,这里就简单看一个插桩source代码的,因为到最后不管是jar还是source,可都是对class文件进行处理~

//com.tencent.matrix.trace.MethodTracer#innerTraceMethodFromSrc
private void innerTraceMethodFromSrc(File input, File output) {ArrayList<File> classFileList = new ArrayList<>();if (input.isDirectory()) {listClassFiles(classFileList, input);} else {classFileList.add(input);}for (File classFile : classFileList) {InputStream is = null;FileOutputStream os = null;try {......if (MethodCollector.isNeedTraceFile(classFile.getName())) {is = new FileInputStream(classFile);ClassReader classReader = new ClassReader(is);ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);//关注这里插桩访问者访问类,然后里面插桩ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);//调用这里后开始插桩classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);......} else {FileUtil.copyFileUsingStream(classFile, changedFileOutput);}} catch (Exception e) {......} finally {.......}}
}

代码里面关键的实现还是asm的classVisitor,我们只需要关注这里面的实现即可~

这里由于封装的路径还是有个两三层,我就简单说下调用关系,这里代码咱们还是看核心实现哈~

//com.tencent.matrix.trace.MethodTracer.TraceMethodAdapter
//1. 调用com.tencent.matrix.trace.MethodTracer.TraceClassAdapter#visitMethod
//2. 调用com.tencent.matrix.trace.MethodTracer.TraceMethodAdapter#TraceMethodAdapter
//3. 利用AdviceAdapter的方法进入和退出回调插桩public final static String MATRIX_TRACE_CLASS = "com/tencent/matrix/trace/core/AppMethodBeat";@Override
protected void onMethodEnter() {//这里的插桩结果就是在方法进入的时候插入 AppMethodBeat.i(methodid);TraceMethod traceMethod = collectedMethodMap.get(methodName);if (traceMethod != null) {traceMethodCount.incrementAndGet();mv.visitLdcInsn(traceMethod.id);mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);}
}@Override
protected void onMethodExit(int opcode) {//方法退出的插桩有判断逻辑TraceMethod traceMethod = collectedMethodMap.get(methodName);if (traceMethod != null) {if (hasWindowFocusMethod && isActivityOrSubClass && isNeedTrace) {TraceMethod windowFocusChangeMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);if (windowFocusChangeMethod.equals(traceMethod)) {//如果是onWindowFocusChanged,那么还会插入AppMethodBeat.at(activity, isFocus)traceWindowFocusChangeMethod(mv, className);}}//无论是不是 onWindowFocusChanged,都会插入AppMethodBeat.o(methodid);traceMethodCount.incrementAndGet();mv.visitLdcInsn(traceMethod.id);mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);}
}

插装部分就是纯工作量的事情了,有了前面的判断和过滤逻辑,这里就非常简单,主要插入 i 方法和 o 方法就行具体可以看上面的代码注释 多余的这里就不展开去看代码了

JavaLib AppMethodBeat 实现

上面就是trace插件在编译时所做的工作,可以看到插桩时期丝毫没有进行任何系统方法的调用,如:SystemClock.time或者System.nanoTime这些获取时间戳的native方法,这样可以理解为一个小优化的点~

不通过系统方法获取时间,matrix利用很巧妙的方式来获取时间,具体就在java lib中去实现,下面我们就分析下java中的实现,其实上面插桩的时候就已经知道具体的实现的核心类是哪个了 => com.tencent.matrix.trace.core.AppMethodBeat

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

我又开发了一个非常好用的开源库

用MotionLayout实现这些不可思议的效果

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

Android 卡顿调研相关推荐

  1. 深入探索Android卡顿优化(下)

    前言 成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~. 在上篇文章中,笔者带领大家学习了卡顿优化分析方法与工具.自动化卡顿检测方案及优化这两块内容. ...

  2. Android卡顿掉帧问题分析之原理篇

    当用户抱怨手机在使用过程中存在卡顿问题的时候,会严重影响用户对手机品牌的好感和应用APP的体验,从而导致用户对手机品牌的忠诚度降低或应用APP的装机留存率下降.所以无论是手机设备厂商还是应用APP开发 ...

  3. Android 卡顿优化之 Skipped * frames 掉帧的计算

    Android 卡顿优化之 Skipped * frames 掉帧的计算 有时候看日志的时候,可能会在日志中看到类似下文的打印: Skipped 30 frames! The application ...

  4. 深入探索Android卡顿优化

    由于卡顿优化这一主题包含的内容太多,为了更详细地进行讲解,因此,笔者将它分为了上.下两篇.本篇,即为<深入探索Android卡顿优化>的上篇. 本篇包含的主要内容如下所示: 卡顿优化分析方 ...

  5. Android卡顿掉帧问题分析之工具篇

    Android卡顿掉帧问题分析之原理篇 Android卡顿掉帧问题分析之工具篇 Android卡顿掉帧问题分析之实战篇 Android卡顿掉帧问题分析之原理篇 公众号:Android技术之家Andro ...

  6. Android卡顿检测及优化

    前言 之前在项目中做过一些Android卡顿以及性能优化的工作,但是一直没时间总结,趁着这段时间把这部分总结一下. 卡顿 在应用开发中如果留意到log的话有时候可能会发下下面的log信息: I/Cho ...

  7. 深入解析:Android卡顿检测及优化项目实战经验总结,任君白嫖

    前言 之前在项目中做过一些Android卡顿以及性能优化的工作,但是一直没时间总结,趁着这段时间把这部分总结一下. GitHub系统教程学习地址:https://github.com/Timdk857 ...

  8. 广研Android卡顿监控系统

    实现背景 应用的使用流畅度,是衡量用户体验的重要标准之一.Android 由于机型配置和系统的不同,项目复杂App场景丰富,代码多人参与迭代历史较久,代码可能会存在很多UI线程耗时的操作,实际测试时候 ...

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

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

最新文章

  1. 北京/上海/深圳内推 | 百度视觉技术团队招聘视觉/3D算法工程师
  2. java红黑树_JAVA学习-红黑树详解
  3. 深入分析存储器的位宽及与C的关系
  4. 0.1uf与47uf并联_UF是什么形式?
  5. [Ajax] 超于json2.js的版本json3.js
  6. js 数字格式化,只能输入正负整数,小数
  7. PPPoE原理和实验
  8. 软考网络工程师备考经验
  9. ansys linux17.2 字体,ubuntu16.04安装Ansys17.2教程,及遇到的问题(安装非完美)
  10. FLASH动画之制作动画
  11. 大学excel题库含答案_大学生计算机基础excel试题及答案
  12. 看小伙如何跟反爬抗争到底
  13. 简单易懂!推荐给自学python的小项目实战!
  14. 零基础学习CANoe Panel(13)—— 滑条(TrackBar )
  15. Django(投票系统项目)
  16. 使用echarts的3D地图中的map3D与scatter3D混合使用时出现坐标位移的情况
  17. 基于Transformer实现电影评论星级分类任务
  18. uname -a详细解释
  19. JQuery与Ajax(上)
  20. 【目标管理】OKR如何助力目标管理?

热门文章

  1. CSGO搬砖项目详细拆解教程,月入破万长期稳定
  2. MySQL删除或清空表中数据的方法
  3. 【2012校园招聘】中兴
  4. Solid Edge与UG格式互换问题
  5. vue2核心以及面试题讲解(组件通信方式,分页器,防抖节流)
  6. 全网Oracle基础最全教程
  7. 开好迭代回顾会议的5个原则
  8. sqlite官方网站
  9. 【分享】常用音乐软件的选择
  10. vue 父组件调用子组件方法ref