Android 教你如何发现 APP 卡顿

最近部门打算优化下 APP 在低端机上的卡顿情况,既然想优化,就必须获取卡顿情况,那么如何获取卡顿情况就是本文目的。

一般主线程过多的 UI 绘制、大量的 IO 操作或是大量的计算操作占用 CPU,导致 App 界面卡顿。只要我们能在发生卡顿的时候,捕捉到主线程的堆栈信息和系统的资源使用信息,即可准确分析卡顿发生在什么函数,资源占用情况如何。那么问题就是如何有效检测 Android 主线程的卡顿发生?

用 adb 系统工具观察 App 的卡顿数据情况,试图重现场景来定位问题。

常用的方式是使用 adb SurfaceFlinger 服务和 adb gfxinfo 功能,在自动化操作 app 的过程中,使用 adb 获取数据来监控 app 的流畅情况,发现出现出现卡顿的时间段,寻找出现卡顿的场景和操作。

方式1:adb shell dumpsysSurfaceFlinger

使用 ‘adb shell dumpsysSurfaceFlinger’ 命令即可获取最近 127 帧的数据,通过定期执行 adb 命令,获取帧数来计算出帧率 FPS。

方式2:adb shell dumpsys gfxinfo

使用 ‘adb shell dumpsys gfxinfo’ 命令即可获取最新 128 帧的绘制信息,详细包括每一帧绘制的 Draw,Process,Execute 三个过程的耗时,如果这三个时间总和超过 16.6ms 即认为是发生了卡顿。

已有的两种方案比较适合衡量回归卡顿问题的修复效果和判断某些特定场景下是否有卡顿情况,然而,这样的方式有几个明显的不足:

一般很难构造实际用户卡顿的环境来重现;

这种方式操作起来比较麻烦,需编写自动化用例,无法覆盖大量的可疑场景,测试重现耗时耗人力;

无法衡量静态页面的卡顿情况;

出现卡顿的时候app无法及时获取运行状态和信息,开发定位困难。

随着对Android 源码的深入研究,也有了其他两种比较方便的方式,并且这两种方式侵入性小,占用内存低,能够更好的用在实际场景中:

利用UI线程的Looper打印的日志匹配;

使用Choreographer.FrameCallback

利用 UI 线程的 Looper 打印的日志匹配

Android 主线程更新 UI。如果界面1秒钟刷新少于 60 次,即 FPS 小于 60,用户就会产生卡顿感觉。简单来说,Android 使用消息机制进行 UI 更新,UI 线程有个 Looper,在其 loop方法中会不断取出 message,调用其绑定的 Handler 在 UI 线程执行。如果在 handler 的 dispatchMesaage 方法里有耗时操作,就会发生卡顿。

下面来看下 Looper.loop( ) 的源码

public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; // Make sure the identity of this thread is that of the local process, // and keep track of what that identity token actually is. Binder.clearCallingIdentity(); final long ident = Binder.clearCallingIdentity(); // Allow overriding a threshold with a system prop. e.g. // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start' final int thresholdOverride = SystemProperties.getInt("log.looper." + Process.myUid() + "." + Thread.currentThread().getName() + ".slow", 0); boolean slowDeliveryDetected = false; for (;;) { Message msg = queue.next(); // might block if (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 logger final Printer logging = me.mLogging; if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } // Make sure the observer won't change while processing a transaction. final Observer observer = sObserver; final long traceTag = me.mTraceTag; long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs; long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs; if (thresholdOverride > 0) { slowDispatchThresholdMs = thresholdOverride; slowDeliveryThresholdMs = thresholdOverride; } final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0); final boolean logSlowDispatch = (slowDispatchThresholdMs > 0); final boolean needStartTime = logSlowDelivery || logSlowDispatch; final boolean needEndTime = logSlowDispatch; if (traceTag != 0 && Trace.isTagEnabled(traceTag)) { Trace.traceBegin(traceTag, msg.target.getTraceName(msg)); } final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0; final long dispatchEnd; Object token = null; if (observer != null) { token = observer.messageDispatchStarting(); } long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid); try { msg.target.dispatchMessage(msg); if (observer != null) { observer.messageDispatched(token, msg); } dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; } catch (Exception exception) { if (observer != null) { observer.dispatchingThrewException(token, msg, exception); } throw exception; } finally { ThreadLocalWorkSource.restore(origWorkSource); if (traceTag != 0) { Trace.traceEnd(traceTag); } } if (logSlowDelivery) { if (slowDeliveryDetected) { if ((dispatchStart - msg.when) <= 10) { Slog.w(TAG, "Drained"); slowDeliveryDetected = false; } } else { if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery", msg)) { // Once we write a slow delivery log, suppress until the queue drains. slowDeliveryDetected = true; } } } if (logSlowDispatch) { showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg); } if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } // Make sure that during the course of dispatching the // identity of the thread wasn't corrupted. final long newIdent = Binder.clearCallingIdentity(); if (ident != newIdent) { Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x" + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass().getName() + " " + msg.callback + " what=" + msg.what); } msg.recycleUnchecked(); } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wspzfw.html