卡顿监控
性能监控
监控应用卡顿方案
Looper Printer 替换方案
很常见的方案,使用系统方法 setMessageLogging
替换掉主线程 Looper 的 Printer 对象,通过计算 Printer 打印日志的时间差,来拿到系统 dispatchMessage
方法的执行时间
1
2
3
Looper.getMainLooper().setMessageLogging(str -> {
// 计算相邻两次日志时间间隔
});
这种方式的优点就是:实现简单,不会漏报。
缺点就是,一些类型的卡顿无法被监控到。
源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Looper.java
private static boolean loopOnce(final Looper me, final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}
// 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);
}
// …
msg.target.dispatchMessage(msg);
// …
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
通过代码可看到,仅监控 dispatchMessage
并不能 cover 住所有卡顿,mQueue.next
注释很清楚了,might block。其中包括:nativePollOnce
方法和 idler.queueIdle()
方法。
nativePollOnce
方法很重要,除了主线程空闲时会阻塞在这里,view 的 touch 事件也都是在这里被处理的。所以如果应用内包含了很多自定义 view,或处理了很多 onTouch 事件,就很难接受了
不仅这样,Native Message 也会卡在 nativePollOnce 方法内,所以同样无法监控到。
queueIdle() 方法会在主线程空闲的时候被调用,所以如果我们在这里有耗时操作,也有可能引起卡顿的,而这种卡顿同样无法监控。
另一种引起卡顿的场景:就是常说的同步屏障了(第一次听到这个名字一脸懵逼)。我们 Message 默认都是同步消息,当我们调用 invalidate 来刷新 UI 时,最终都会调用到 ViewRootImpl#scheduleTraversals
方法,会向主线程 Looper postSyncBarrier
插入同步屏障消息,目的是刷新 UI 时,让 Looper 中的同步消息都被跳过,使渲染 UI 的同步屏障消息得到优先处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ActivityThread.java
@UnsupportedAppUsage
void scheduleGcIdler() {
if (!mGcIdlerScheduled) {
mGcIdlerScheduled = true;
Looper.myQueue().addIdleHandler(mGcIdler);
}
mH.removeMessages(H.GC_WHEN_IDLE);
}
void unscheduleGcIdler() {
if (mGcIdlerScheduled) {
mGcIdlerScheduled = false;
Looper.myQueue().removeIdleHandler(mGcIdler);
}
mH.removeMessages(H.GC_WHEN_IDLE);
}
为啥说同步屏障会引起卡顿了,根据代码可看到,scheduleTraversals 方法和 unscheduleTraversals 是配对的,但都不是线程安全的方法。如果在异步线程 invalidate,导致多次执行 scheduleTraversals 方法,而 unscheduleTraversals 又只能移除最后的 mTraversalBarrier,那就会造成主线程的 Looper 的同步消息一直得不到处理,从而引起卡死。
缺点
但有一些卡顿仍然无法监控到:
- queue.next() 卡顿
nativePollOnce 方法很重要,除了主线程空闲时会阻塞在这里,view 的 touch 事件也都是在这里被处理的。所以如果应用内包含了很多自定义 view,或处理了很多 onTouch 事件,就很难接受了。不仅这样,Native Message 也会卡在 nativePollOnce 方法内,所以同样无法监控到。
- IdleHandler 卡顿
在 MessageQueue 消息列表都执行完的时候,会执行 idleHandlers 中的消息,而 idleHandler 也有可能产生卡顿,并且 Looper printer 无法监控到,我们可以反射 MessageQueue 中的 mIdleHandlers 对象,这个变量保存了所有将要执行的 IdleHandler,我们只需要把 ArrayList 类型的 mIdleHandlers,通过反射,替换为 MyArrayList,在我们自定义的 MyArrayList 中重写 add 方法,再将我们自定义的 MyIdleHandler 添加到 MyArrayList 中,就完成了 “ 偷天换日 “。从此之后 MessageQueue 每次执行 queueIdle 回调方法,都会执行到我们的 MyIdleHandler 中的的 queueIdle 方法,就可以在这里监控 queueIdle 的执行时间了
- SyncBarrier 卡顿
SyncBarrier 卡顿表现为同步消息无法执行,这种情况我们可以通过去获取队列中 message.target = null 的对象(同步屏障),判断该对象的 when 属性是否存在超时,如果该同步屏障已经存在很久了,我们可以再进一步验证是否是 SyncBarrier 发生了泄露,为什么需要进一步验证, 因为别的卡顿情况也会导致 SyncBarrier 一直存在队列中,我们可以分别发送一个异步消息和一个同步消息,如果异步消息被消费了而同步消息未消费,说明 SyncBarrier 发生了泄露,此时我们甚至可以尝试去将该 SyncBarrier 从 MessageQueue 中 remove 掉。
解决
作为一个主流的监控方案,一些缺陷已经有了解决方案
nativePollOnce 的 onTouchEvent 监控
可以通过 ELF Hook, hook 到 libinput.so
的 recvform
和 sendto
方法,用我们自己的方法替换,在这里做监控,当调用 recvform 方法时,说明我们的应用接收到了 onTouch 事件,当被调用 sendto 方法时,说明 onTouch 事件已经被消费。
IdleHandler queueIdle 监控
ArrayList mIdleHandlers 保存着全部我们所需的 IdleHandler,那么我们完全可以通过反射赋值成我们自己的 MyArrayList,并重写 MyArrayList 的 add 方法,是不是就可以监控到每个被添加的 IdleHandler 呢?
1
2
3
4
5
6
7
8
9
10
11
// MessageQueue.java
Message next() {
for (;;) {
// ...
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
// ...
}
}
在 add 方法内拿到被添加的 IdleHandler 后,我们就可以监控 queueIdle 方法执行的时间了,代码片段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static class MyArrayList<E> extends ArrayList {
@Override
public boolean add(Object o) {
if (o instanceof MessageQueue.IdleHandler) {
super.add(new MyIdleHandler((MessageQueue.IdleHandler)o));
}
return super.add(o);
}
}
static class MyIdleHandler implements MessageQueue.IdleHandler {
private final MessageQueue.IdleHandler idleHandler;
MyIdleHandler(MessageQueue.IdleHandler idleHandler) {
this.idleHandler = idleHandler;
}
@Override
public boolean queueIdle() {
// 监控 idleHandler.queueIdle() 耗时即可
return this.idleHandler.queueIdle();
}
}
同步屏障卡死监控
可以定时的通过反射去拿 MessageQueue 的 mMessages
,如果发现 mMessages.target=null
,并且 mMessages.when
已经很长时间了,就有可能发生同步屏障消息泄漏了,这时我们可以再主动向主线程 Looper 发送一个同步消息和一个异步消息,如果同步消息无法执行,但异步消息被处理,这时基本可以确定泄漏了。
我们可以通过反射去 removeSyncBarrier(token)
,其中 token
为 mMessages.arg1
。
基于 WatchDog
参考系统的 WatchDog 原理,我们启动一个卡顿检测线程,该线程定期的向 UI 线程发送一条延迟消息,执行一个标志位加 1 的操作,如果规定时间内,标志位没有变化,则表示产生了卡顿。如果发生了变化,则代表没有长时间卡顿,我们重新执行延迟消息即可。
该方案并不能完全检测到所有的设定条件内的卡顿问题,但可以配合 Handler Printer 替换的方案来实现交叉覆盖,基本可以满足我们的需求
帧率监控 Choreographer
Android 从 4.1 开始加入 Choreographer 用于同 VSync 机制配合,实现统一调度绘制界面。我们可以设置 Choreographer 类的 FrameCallback 函数,当每一帧被渲染时会触发 FrameCallback 回调,FrameCallback 回调 doFrame(long frameTimeNanos) 函数,一次界面渲染会回调 doFrame,如果两次 doFrame 间隔大于 16.6ms 则发生了卡顿。而 1s 内有多少次 callback,就代表了实际的帧率。
1
2
3
4
5
6
7
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
// 这里可以统计相邻间隔,判断卡顿,也可以统计doFrame 帧率
Choreographer.getInstance().postFrameCallback(this);
}
});
跟卡顿不同的是,需要排除掉页面没有操作的情况,我们应该只在界面存在绘制的时候才做统计。那么如何监听界面是否存在绘制行为呢?可以通过 addOnDrawListener 实现。
getWindow().getDecorView().getViewTreeObserver().addOnDrawListener
这种方式优点:使用简单,不仅支持卡顿监控,还支持计算帧率。
缺点就是:需要另开子线程来获取堆栈信息,会消耗部分系统资源。
ANR 监控
SIGQUIT 信号
当应用发生 ANR 后,system_server 进程会发送 SIGQUIT 信号来通知相关进程来 dump 堆栈,这里就包括发生 ANR 的应用进程(且是第一个收到通知的进程),除此外,还有一些 AMS 维护的 LRU list 内的进程(CPU 使用率高的进程)和一些固定的 native 系统进程,也都会收到通知并 dump 堆栈。
微信的 Matrix
和 XCrash
库就是运用的这个原理,使用 sigaction 方法注册 signal handler 来监听 SIGQUIT 信号,来达到监控 ANR 的目的,非常巧妙。
在这里需要注意,默认情况下进程通过 SignalCatcher
监听 SIGQUIT
信号,进行堆栈转储生成 ANR Trace 文件。因此当我们监听 SIGQUIT
信号后,需要重新向 SignalCatcher
发送 SIGQUIT
如果缺少重新向 SignalCatcher 发送 SIGQUIT 信号的步骤,Android System 管理服务(AMS)将一直等待 ANR 进程写入堆栈信息。直到超过 20 秒的超时时间,AMS 才会被迫中断,并继续后续流程。这将导致 ANR 弹窗的显示非常缓慢(因为超时时间为 20 秒),同时在 /data/anr 目录下也无法生成完整的 ANR Trace 文件。
误报情况处理
当监听到 SIGQUIT 信号时,不一定是发生了 ANR。Matrix 的文档中提到了两种误报的情况:
- 比如可能是其它进程 ANR 了,发生 ANR 的进程不是唯一需要进行堆栈转储的进程。系统会收集许多其他进程进行堆栈转储,用于生成 ANR Trace 文件
- 厂商或者是开发者自己发送的
SIGQUIT
信号,发送 SIGQUIT 信号其实是很容易的一件事情
因此我们需要在监听到信号时再进行一次检查:在 ANR 弹窗前,会给发生 ANR 的进程标记一个 NOT_RESPONDING
的 flag,而这个 flag 我们可以通过 ActivityManager
来获取:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static boolean checkErrorState() {
try {
Application application = sApplication == null ? Matrix.with().getApplication() : sApplication;
ActivityManager am = (ActivityManager) application.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.ProcessErrorStateInfo> procs = am.getProcessesInErrorState();
if (procs == null) return false;
for (ActivityManager.ProcessErrorStateInfo proc : procs) {
if (proc.pid != android.os.Process.myPid()) continue;
if (proc.condition != ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) continue;
return true;
}
return false;
} catch (Throwable t){
MatrixLog.e(TAG,"[checkErrorState] error : %s", t.getMessage());
}
return false;
}
我们可以在监听到信号时判断当前进程是否被标记为 NOT_RESPONDING 来判断当前进程是否发生了 ANR
漏报情况处理
当进程被标记为 NOT_RESPONDING 时一定发生了 ANR,但是当进程发生了 ANR 时,不一定会被标记为 NOT_RESPONDING
Matrix 的文档中提到了两种漏报情况:
- 后台 ANR(SilentAnr): 后台 ANR 会直接杀死进程,不会走到标记状态的代码
- 厂商定制逻辑: 相当一部分机型 (比如 OPPO、VIVO 两家的高版本 Android ) 修改了 ANR 的逻辑,即使是前台 ANR 也会直接杀死进程
Matrix 通过判断主线程在收到 SIGQUIT 信号时是否处于卡顿状态来判断当前是否发生 ANR,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static boolean isMainThreadStuck(){
try {
MessageQueue mainQueue = Looper.getMainLooper().getQueue();
Field field = mainQueue.getClass().getDeclaredField("mMessages");
field.setAccessible(true);
final Message mMessage = (Message) field.get(mainQueue);
if (mMessage != null) {
long when = mMessage.getWhen();
if(when == 0) {
return false;
}
long time = when - SystemClock.uptimeMillis();
long timeThreshold = BACKGROUND_MSG_THRESHOLD;
if (foreground) {
timeThreshold = FOREGROUND_MSG_THRESHOLD;
}
return time < timeThreshold;
}
} catch (Exception e){
return false;
}
return false;
}
- 通过反射获取主线程
Looper
的mMessage
对象,该消息的when
变量,就表示该消息的入队时间 - 将入队时间与当前时间进行比较,就可以获取该消息的等待时间
- 当等待时间超过一定阈值的话,我们就认为主线程处于阻塞状态,结合 SIGQUIT 信号,判断为发生了 ANR
线程监控
- 插桩将无名字的 Thread 添加名字,在监控的时候能得到更有效的信息
- Java 线程监控,插桩 thread 构造
- Native 线程监控,native hook
pthread_create()
- 线程收敛
- 线程栈帧优化,默认 1 M
- 三方的插件:Booster, ByteX