ANR系列(二)——ANR监听方案之WatchDog

news2024/12/25 9:22:34

前言

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的现象,再采集线程信息

参考资料

  • Android - ANRWatchDog

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/349737.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大漠插件最新中文易语言模块7.2302

模块名称:大漠插件中文模块最新通用7.2302模块简介:大漠插件中文模块最新通用7.2302模块特色:原翻译:花老板完善命令备注:易生易世本人花费一个月时间才将命令完善了插件的备注说明.且用且珍惜去掉了大漠插件定制版类.因为没用.模块特色:什么是中文模块?大漠插件模块是由大漠类…

Java SPI 概念和应用实现

Java SPI 测试 Demo一.SPI 简介1.概念 SPI 与 API2.作用二.Jdk SPI 实现1.SPI 接口定义2.SPI 实现类定义3.SPI 配置4.测试三.SpringBoot SPI 实现1.引入 SpringBoot 依赖2.SpringBoot SPI 配置3.测试一.SPI 简介 1.概念 SPI 与 API SPI 全称&#xff1a;Service Provider Int…

Xshell连接阿里云服务器搭建网站

一、建设一个网站的基本要求 申请一个独立的域名申请一台云服务器ECS在服务器上安装网站环境&#xff0c;如&#xff1a;Apache发布网站内容至云服务器将第一步注册的域解析至云服务器的外网IP地址进行ICP备案 二、用户访问网站的过程 在浏览器上输入域名浏览器自动调用DNS&…

SPI(服务提供发现机制)简单使用

一、概述 SPI的英文全称是Service Provider Interface&#xff0c;是Java内置的一种服务提供发现机制。一般常用于一些框架或组件库的开发&#xff0c;我们最熟悉JDBC就应用到了SPI机制&#xff0c;并且在Spring、Dubbo中也大量应用了SPI机制。SPI机制是针对同一个接口采用不同…

质量小议18 -- 熵

未能深入理解其包含的物理意义&#xff0c;浅记于此&#xff0c;以求理解对抗有自然无序的过程。 熵&#xff0c;对系统无序程序的度量&#xff0c;表征系统混乱程度。系统总是由有序向无序&#xff0c;最后走向静寂。 关键词&#xff1a;1. 无序和混乱&#xff1b;2. 有序向无…

Java中static关键字和代码块的学习

本文介绍了Java中static关键字的使用,即静态成员变量和成员方法以及使用,静态与非静态成员变量和方法的对比总结 Java中的代码块介绍与最后结合代码块和构造方法后的初始化代码执行顺序的练习 static和代码块的学习三.认识static关键字1.static修饰成员变量2.static修饰成员方法…

GAMES101-计算机图形学入门 LEC4: TRANSFORMATION-3D

本节课程视频地址&#xff1a;https://www.bilibili.com/video/BV1X7411F744/?p4 补充上一节课的一个内容&#xff0c;旋转矩阵的逆矩阵是它的转置&#xff0c;也就是说有R−θRθ−1RθTR_{-\theta} R_\theta^{-1}R_\theta^TR−θ​Rθ−1​RθT​ 上节课讲了&#xff0c;…

【go】结合一个go开源项目分析谷歌浏览器cookie为什么不安全 附go项目导包失败怎么解决教程

本文创作背景 源于谷歌浏览器提示密码被泄露 并且某站很快收到了异地企图登录的提醒。 当即怀疑是不是谷歌浏览器保存的密码不安全&#xff0c;最后查阅诸多资料 并找到一个go语言编写的开源项目进行研究&#xff0c;虽然最终不能确定密码是如何泄露的 但研究结论还是让人不由感…

在华为MateBook Ego的arm windows 11上安装hyper-V虚拟机

入手一台华为matebook Ego的笔记本&#xff0c;由于想要测试一些arm的驱动功能&#xff0c;经常会把系统搞蓝屏&#xff0c;于是想安装一个虚拟机&#xff0c;于是试了vmware ,visual-box&#xff0c;由于本机是arm架构上面两个软件都无法进行正常安装&#xff0c;可能是由于有…

Excel+SQL实战项目 - 餐饮业日销售情况分析仪

目录1、要完成的任务2、认识数据3、SQL数据加工4、excel形成分析仪1、要完成的任务 目标&#xff1a;结合SQL和excel实现餐饮业日销售情况分析仪&#xff0c;如下表&#xff1a; 认识分析仪&#xff1a; 切片器&#xff1a;店面 分为四部分&#xff1a;KPI 、组合图、饼图、数…

如何自学芯片设计?

众所周知&#xff0c;芯片设计自学还是比较困难的&#xff0c;更不存在速成的。这里简单说一下学习的规划。 学会相应的知识 无论是科班毕业&#xff0c;还是理工科专业&#xff0c;想要入行IC&#xff0c;那就一定要具备相关的基础知识。尤其是在学校里&#xff0c;学习的很…

Centos7 内核升级

一、背景 在 CentOS 使用过程中&#xff0c;高版本的应用环境可能需要更高版本的内核才能支持&#xff0c;所以难免需要升级内核&#xff0c;所以下面将介绍yum和rpm两种升级内核方式。 关于内核种类: kernel-ml——kernel-ml 中的ml是英文【 mainline stable 】的缩写&…

2W字正则表达式基础知识总结,这一篇就够了!!(含前端常用案例,建议收藏)

正则表达式 (Regular Expression&#xff0c;简称 RE 或 regexp ) 是一种文本模式&#xff0c;包括普通字符&#xff08;例如&#xff0c;a 到 z 之间的字母&#xff09;和特殊字符&#xff08;称为"元字符"&#xff09;正则表达式使用单个字符串来描述、匹配一系列匹…

wpscan常见的使用方法

目录 简单介绍 暴力破解 信息收集 指定用户爆破 命令集合 简单介绍 Wordpress是一个以PHP和MySQL为平台的免费自由开源的博客软件和内容管理系统。 WPScan是Kali Linux默认自带的一款漏洞扫描工具&#xff0c;它采用Ruby编写&#xff0c;能够扫描WordPress网站中的多种安…

微信微店怎么开店铺步骤【微信开店】

商家在微信平台主要是通过什么方式进行卖货呢&#xff0c;大家的答案都会是微信小店、小程序微店铺之类的&#xff0c;的确微信店铺是商家在微信平台上重要的卖货渠道&#xff0c;那么微信微店怎么开店铺&#xff0c;下面就给大家分享微信微店怎么开店铺步骤。 一、准备好资料…

Netty启动流程源码剖析

案例 本文利用natty-all-source 包下的的demo案例 echo来分析下源码&#xff0c;代码如下&#xff1a;server 端代码 /*** Echoes back any received data from a client.*/ public final class EchoServer {static final boolean SSL System.getProperty("ssl") …

day14_oop_抽象_接口

今日内容 上课同步视频:CuteN饕餮的个人空间_哔哩哔哩_bilibili 同步笔记沐沐霸的博客_CSDN博客-Java2301 零、 复习昨日 一、作业 二、抽象 三、接口 零、 复习昨日 多态的好处: 扩展性强.加入新的功能,不需要改动代码降低代码耦合度(解耦合或者松耦合) 一、抽象类 1.1 抽象类…

C# VS2010 Winform 学习笔记遇见问题

参考书本《Visual C# .Net程序设计与应用开发》 学习C#&#xff1a; 对象的封装性&#xff1a;通过get()、set()函数读写。 1.Visual C#面向对象编程中的继承、多态。 2.enum&#xff1a;枚举&#xff0c;array.copy方法&#xff1a;数组拷贝&#xff0c;public static voi…

以应用为导向,看声纹识别中的音频伪造问题

声纹识别&#xff0c;又称说话人识别&#xff0c;是根据语音信号中的声纹特征来识别话者身份的过程&#xff0c;也是一种重要的生物认证手段。历经几十年的研究&#xff0c;当前声纹识别系统已取得了令人满意的性能表现&#xff0c;并在安防、司法、金融、家居等诸多领域中完成…

jenkins构建报错:.java:16: error: package javafx.util does not exist

1、报错 jenkins构建报错 package javafx.util does not exist2、报错原因 代码发现使用了javafx类&#xff0c;该类仅存在OracleJDK中&#xff0c;OpenJDK中没有该类。 jenkins服务器安装的是openjdk 3、卸载OpenJDK 具体不概述了 4、离线安装OracleJDK 1&#xff09;…