前言
ANR的监控在Android6.0之前可以通过监听文件data/anr/trace
读取trace信息来分析,但从6.0之后就被禁止了。随着Android的发展,手机里的ANR越来越多,对ANR的监控方案也就五花八门。
WatchDog方案
WatchDog是个开源的框架,是一个短小精悍的UI卡顿监测框架,只有2个源文件,ANRWatchDog
和ANRError
。
1、WatchDog核心原理
启动一个异步线程,在while循环中,使用主线程的Handler发送一个消息,线程休眠指定的时间5s,当线程唤醒之后,如果发送的消息还没被主线程执行,即认为主线程发生了卡顿。
- 成员变量
- _anrListener:当监控到ANR时的回调方法。如打印日志、崩溃抛出异常
- _anrInterceptor:当监控到ANR时候,判断是否拦截该ANR。如果拦截则不会触发ANRListener,暴露给开发者使用
- _timeoutInterval:默认的线程卡顿时间为5秒
- _uiHandler:主线程的Handler
- _tick:判断ANR是否发生的开关,非0则没收到主线程消息,即ANR超时,0就收到主线程消息
- _reported:指当前ANR是否报告过,报告过则忽略
- _namePrefix:ANR发生时,表示统计哪个线程的堆栈信息,如果是null则表示主线程堆栈信息
- 核心原理
ANRWatchDog
继承自Thread,分析其原理,则从启动ANRWatchDog线程开始,执行ANRWatchDog$run()
public class ANRWatchDog extends Thread {
private final Runnable _ticker = new Runnable() {
@Override public void run() {
_tick = 0;
_reported = false;
}
};
@Override
public void run() {
// 1、设置线程名称
setName("|ANR-WatchDog|");
// 2、获取线程休眠时间,即 UI 卡顿超时时间。默认是5000
long interval = _timeoutInterval;
// 3、如果线程中断,则直接退出线程。
while (!isInterrupted()) {
boolean needPost = _tick == 0;
// 4、_tick置为非0,等待主线程改为0
_tick += interval;
// 5、如果主线程一直在阻塞的话,就不要一直发消息。如果主线程未阻塞,发送消息。
if (needPost) {
_uiHandler.post(_ticker);
}
try {
// 6、休眠指定的时间。
Thread.sleep(interval);
} catch (InterruptedException e) {
// 如果线程休眠过程中,线程被中断,则回调 onInterrupted() 方法。
_interruptionListener.onInterrupted(e);
return ;
}
// 7、睡眠过后,如果主线程还没有把_tick置为0,就认为发生ANR。
if (_tick != 0 && !_reported) {
// 如果 _ignoreDebugger 为 false,且 AndroidStudio 正在断点调试,则忽略 ANR
if (!_ignoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) {
Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
_reported = true;
continue ;
}
// 拦截器是否拦截 ANR
interval = _anrInterceptor.intercept(_tick);
if (interval > 0) {
continue;
}
final ANRError error;
// _namePrefix!=null 表示统计所有线程或者统计指定 _namePrefix 前缀的线程堆栈信息。
if (_namePrefix != null) {
error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);
} else {
// _namePrefix ==null 表示只统计主线程堆栈信息。
error = ANRError.NewMainOnly(_tick);
}
// 8、回调onAppNotResponding。
_anrListener.onAppNotResponding(error);
interval = _timeoutInterval;
_reported = true;
}
}
}
}
最终通过ANRError.New
或者ANRError.NewMainOnly
生成所有线程或者主线程的堆栈信息抛出来。作者通过继承Throwable
的形式,并覆写fillInStackTrace()
让系统不要自己收集堆栈信息,系统这个方法耗时太多,而是通过Thread.getAllStackTraces()
获取所有线程,并从线程中拿出他们当前的堆栈信息即可。
public class ANRError extends Error {
// 方法一:生成所有堆栈信息
static ANRError New(long duration, String prefix, boolean logThreadsWithoutStackTrace) {
// 获取主线程
final Thread mainThread = Looper.getMainLooper().getThread();
// 将主线程的堆栈信息,排到到第一位输出
final Map<Thread, StackTraceElement[]> stackTraces = new TreeMap<Thread, StackTraceElement[]>(new Comparator<Thread>() {
@Override
public int compare(Thread lhs, Thread rhs) {
if (lhs == rhs)
return 0;
if (lhs == mainThread)
return 1;
if (rhs == mainThread)
return -1;
return rhs.getName().compareTo(lhs.getName());
}
});
// 获取所有线程,并根据传递过来的参数,对线程信息进行过滤
for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet())
if (
entry.getKey() == mainThread || (entry.getKey().getName().startsWith(prefix) && (logThreadsWithoutStackTrace || entry.getValue().length > 0))
)
stackTraces.put(entry.getKey(), entry.getValue());
// 主线程信息加进来
if (!stackTraces.containsKey(mainThread)) {
stackTraces.put(mainThread, mainThread.getStackTrace());
}
$._Thread tst = null;
for (Map.Entry<Thread, StackTraceElement[]> entry : stackTraces.entrySet())
tst = new $(getThreadTitle(entry.getKey()), entry.getValue()).new _Thread(tst);
return new ANRError(tst, duration);
}
// 方法二:生成主线程的堆栈信息
static ANRError NewMainOnly(long duration) {
final Thread mainThread = Looper.getMainLooper().getThread();
final StackTraceElement[] mainStackTrace = mainThread.getStackTrace();
return new ANRError(new $(getThreadTitle(mainThread), mainStackTrace).new _Thread(null), duration);
}
private static String getThreadTitle(Thread thread) {
return thread.getName() + " (state = " + thread.getState() + ")";
}
}
2、WatchDog问题
WatchDog虽然思路很巧妙,但是ANR统计并不准确
- 例如ANRWatchDog线程休眠指定的时间为5秒,线程阻塞8秒
- 当第1s发送的时候,此时主线程处于空闲状态,马上回包表示没问题
- 当第5s发送的时候,此时主线程处于阻塞状态,但是到第8s,恢复了空闲状态,马上回包表示没问题
- 中间的8s阻塞就被忽略掉了
3、WatchDog改进
- 可以考虑针对发送到主线程的消息做个策略,将原来的5s发送一次改为1s发送一次,假如累计有5次发出且5次都回不了包,则表示有ANR的现象,再采集线程信息
参考资料
- Android - ANRWatchDog