ANR系列——ANR监听方案之WatchDog

Posted 许英俊潇洒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ANR系列——ANR监听方案之WatchDog相关的知识,希望对你有一定的参考价值。

前言

ANR的监控在android6.0之前可以通过监听文件data/anr/trace读取trace信息来分析,但从6.0之后就被禁止了。随着Android的发展,手机里的ANR越来越多,对ANR的监控方案也就五花八门。

WatchDog方案

WatchDog是个开源的框架,是一个短小精悍的UI卡顿监测框架,只有2个源文件,ANRWatchDogANRError

1、WatchDog核心原理

启动一个异步线程,在while循环中,使用主线程的Handler发送一个消息,线程休眠指定的时间5s,当线程唤醒之后,如果发送的消息还没被主线程执行,即认为主线程发生了卡顿。

  • 成员变量
  1. _anrListener:当监控到ANR时的回调方法。如打印日志、崩溃抛出异常
  2. _anrInterceptor:当监控到ANR时候,判断是否拦截该ANR。如果拦截则不会触发ANRListener,暴露给开发者使用
  3. _timeoutInterval:默认的线程卡顿时间为5秒
  4. _uiHandler:主线程的Handler
  5. _tick:判断ANR是否发生的开关,非0则没收到主线程消息,即ANR超时,0就收到主线程消息
  6. _reported:指当前ANR是否报告过,报告过则忽略
  7. _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改进

  1. 可以考虑针对发送到主线程的消息做个策略,将原来的5s发送一次改为1s发送一次,假如累计有5次发出且5次都回不了包,则表示有ANR的现象,再采集线程信息

参考资料

以上是关于ANR系列——ANR监听方案之WatchDog的主要内容,如果未能解决你的问题,请参考以下文章

ANR系列——ANR监听方案之SyncBarrier

ANR系列之二:Input类型ANR产生原理讲解

ANR系列:如何分析ANR和避免ANR?

ANR系列:Service触发ANR的源码分析

ANR系列:不同组件多长时间后触发ANR?

ANR系列:广播触发ANR的原理