YCAndroidTool

Introduction: 用于项目测试,崩溃记录日志【可以查看,分享】和重启【多种重启 app 方式】,性能检测,网路拦截查看的工具小助手。提高开发效率……
More: Author   ReportBugs   
Tags:

目录介绍

  • 01.该库具有的功能
  • 02.崩溃处理模块
  • 03.网络分析库模块
  • 04.ping 库模块
  • 05.该库如何使用
  • 06.后续的需求说明
  • 07.异常栈轨迹原理
  • 08.部分问题反馈
  • 09.其他内容说明

01.该库具有的功能

  • 崩溃处理相关模块
    • 崩溃重启操作,崩溃记录日志操作,崩溃日志列表支持查询,删除,查看详情,分享,保存文本,以及截图等操作。
  • 网络分析库模块
    • 网络流程分析,记录每个网络请求->响应数据,方便查看很全面的请求头信息,响应头信息,以及 body 实体,以及网络连接,dns 解析,TLS 连接,请求响应等时间差……
  • ping 库模块
    • 通过 ping 检测网络问题,帮助诊断,这个在 Android 中检查域名的诊断信息……
  • 最大特点
    • 入侵性低,你不用改动愿项目代码,几行代码设置即可使用这几个模块功能,已经用于多个实际项目中。如果觉得可以,麻烦 star 一下……

02.崩溃处理模块

2.1 异常崩溃介绍

  • 异常崩溃后思考的一些问题
    • 1.是否需要恢复 activity 栈,以及所在崩溃页面数据
    • 2.crash 信息保存和异常捕获,是否和百度 bug 崩溃统计 sdk 等兼容。是否方便接入
    • 3.是否要回到栈顶部的那个 activity(保存栈信息)
    • 4.崩溃后需要收集哪些信息。手机信息,app 信息,崩溃堆栈,内存信息等
    • 5.异常崩溃如何友好退出,以及崩溃后调用重启 app 是否会出现数据异常
    • 6.针对 native 代码崩溃,如何记录日志写到文件中
  • 该库可以做一些什么
    • 1.在 Android 手机上显示闪退崩溃信息,并且崩溃详情信息可以保存,分享给开发
      • 主要是测试同学在测试中发现了崩溃,然后跑过去跟开发说,由于不容易复现导致开发童鞋不承认……有时候用的 bug 统计不是那么准!
    • 2.对于某些设备,比如做 Kindle 开发,可以设置崩溃重启 app 操作
    • 3.暴露了用户上传自己捕获的 crash 数据,以及崩溃重启的接口监听操作
    • 4.一个崩溃日志保存到一个文件中,文件命名规则【版本+日期+异常】:V1.0_2020-09-02_09:05:01_java.lang.NullPointerException.txt
    • 5.崩溃日志 list 可以获取,支持查看日志详情,并且可以分享,截图,以及复制崩溃信息
    • 6.收集崩溃日志包括,设备信息,进程信息,崩溃信息(Java 崩溃、Native 崩溃 or ANR)
    • 7.收集崩溃时的内存信息(OOM、ANR、虚拟内存耗尽等,很多崩溃都跟内存有直接关系),完善中

2.2 截图如下所示

image image image image

2.3 崩溃后日志记录

image

2.4 崩溃流程图

image

2.5 该库优势分析

  • 低入侵性接入该 lib,不会影响你的其他业务。暴露崩溃重启,以及支持开发者自己捕获 crash 数据的接口!能够收集崩溃中的日志写入文件,记录包括设备信息,进程信息,崩溃信息(Java 崩溃、Native 崩溃 or ANR),以及崩溃时内存信息到 file 文件中。支持用户获取崩溃列表,以及跳转崩溃日志详情页面,并且可以将崩溃日志分享,截长图,复制等操作。可以方便测试和产品给开发提出那种偶发性 bug 的定位日志,免得对于偶发行崩溃,开发总是不承认……开发总是不承认……

03.网络分析库模块

3.1 网络分析库模块

  • 参考 stetho 库地址
  • 功能
    • Stetho 是 Facebook 开源的一个 Android 调试工具。
    • 是一个 Chrome Developer Tools 的扩展,可用来检测应用的网络、数据库、WebKit 、SharePreference 等方面的功能。
    • 开发者也可通过它的 dumpapp 工具提供强大的命令行接口来访问应用内部。
  • 那么既然网络请求添加 StethoInterceptor,既可以拦截网络请求和响应信息,发送给 Chrome。那么能不能自己拿来用……
    • 可以的,直接致敬 Stetho 库,扒代码
  • StethoInterceptor 大概流程
    • 整个流程我们可以简化为:发送请求时,给 Chrome 发了条消息,收到请求时,再给 Chrome 发条消息(具体怎么发的可以看 NetworkEventReporterImpl 的实现)
    • 两条消息通过 EventID 联系起来,它们的类型分别是 OkHttpInspectorRequest 和 OkHttpInspectorResponse,两者分别继承自 NetworkEventReporter.InspectorRequest 和 NetworkEventReporter.InspectorResponse。
    • 我们只要也继承自这两个类,在自己的网络库发送和收到请求时,构造一个 Request 和 Response 并发送给 Chrome 即可。
  • 如何拿来用
    • 既然 Android 中使用到 facebook 的 stetho 库,可以拦截手机请求请求,然后去 Chrome 浏览器,在浏览器地址栏输入:chrome://inspect 。即可查看请求信息。
    • 那么能不能把这个拿到的请求信息,放到集合中,然后在 Android 的页面中展示呢?这样方便开发和测试查看网络请求信息,以及请求流程中的消耗时间(比如 dns 解析时间,请求时间,响应时间,共耗时等等)
  • OkHttp 如何进行各个请求环节的耗时统计呢?
    • OkHttp 版本提供了 EventListener 接口,可以让调用者接收一系列网络请求过程中的事件,例如 DNS 解析、TSL/SSL 连接、Response 接收等。
    • 通过继承此接口,调用者可以监视整个应用中网络请求次数、流量大小、耗时(比如 dns 解析时间,请求时间,响应时间等等)情况。
    • 因此统计耗时,需要致敬 EventListener 实现,照搬代码即可。
  • 如何消耗记录时间
    • 在 OkHttp 库中有一个 EventListener 类。该类是网络事件的侦听器。扩展这个类以监视应用程序的 HTTP 调用的数量、大小和持续时间。
    • 所有启动/连接/获取事件最终将接收到匹配的结束/释放事件,要么成功(非空参数),要么失败(非空可抛出)。
    • 比如,可以在开始链接记录时间;dns 开始,结束等方法解析记录时间,可以计算 dns 的解析时间。
    • 比如,可以在开始请求记录时间,记录 connectStart,connectEnd 等方法时间,则可以计算出 connect 连接时间。

3.2 网络拦截库功能

  • 网络请求拦截
    • 记录每个页面网络请求的数据,保存在 map 中,方便查看网络请求的请求头,响应头,响应 body
    • 针对获取的网络数据,记录网络状态,请求耗时,请求方式,完善了网络日志包括的绝大部分字段信息展示
  • 流量统计
    • 目前只是针对 get,post 网络请求以及上传流量统计,并且针对 get 和 post 比例展示,以及抓包数量统计
    • 后期完善比如 glide 加载图片消耗的流量统计
  • 网络消耗时间
    • 网络请求到完成耗时,dns 消耗时间,连接消耗时间,TLS 连接开始结束消耗时间,request 请求消耗时间,response 响应消耗时间等等
  • 设备信息
    • 获取手机设备信息,包括硬件厂商,版本,root,包名等信息
    • 获取本机 wifi 信息,主要是 wifi 的名称,mac 地址,ip 地址,dns 信息,子网掩码等
    • 获取服务端信息,根据网络请求 host,获取服务端 ip,mac,是 ipv4 还是 ipv6 等,这个还在完善中
  • ping 网络诊断
    • 致敬网易 ping 方案,应用于 Android 项目中,获取 Android 项目域名,拿到 ping 信息
  • 全局悬浮按钮
    • 为了方便查看每个 activity 页面的网络请求数据,因此在每个 activity 页面显示全局悬浮按钮,点击即可跳入查看数据页面
    • 这个一行代码设置即可,无任何耦合作用,低入侵

3.3 网络请求接口信息

  • 请求接口如下所示
  • General
  • Response Header
    • HTTP/1.1 200 OK
    • Server: Apache-Coyote/1.1
    • Cache-Control: private
    • Expires: Thu, 01 Jan 1970 08:00:00 CST
    • Content-Type: application/json;charset=UTF-8
    • Transfer-Encoding: chunked
    • Date: Thu, 10 Sep 2020 01:05:47 GMT
  • Request Header
    • Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9
    • Accept-Encoding: gzip, deflate, br
    • Accept-Language: zh-CN,zh;q=0.9
    • Cache-Control: no-cache
    • Connection: keep-alive
    • Cookie: JSESSIONID=5D6302E64E9734210FA231A6FAF5799E; Hm_lvt_90501e13a75bb5eb3d067166e8d2cad8=1598920692,1599007288,1599094016,1599629553; Hm_lpvt_90501e13a75bb5eb3d067166e8d2cad8=1599699419
    • Host: www.wanandroid.com
    • Pragma: no-cache
    • Sec-Fetch-Dest: document
    • Sec-Fetch-Mode: navigate
    • Sec-Fetch-Site: none
    • Upgrade-Insecure-Requests: 1
    • User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36
  • Response 返回 body
    • 这里省略
  • 看截图如下
    • image

3.5 案例截图如下

image image image image image

04.ping 库模块

4.1 ping 在 Android 的应用

  • 为了检查网络,在 android 上也可以通过 ping 来查看是否网络通。
  • 实现方案有哪些
    • 通过后台线程执行 ping 命令的方式模拟 traceroute 的过程,缺点就是模拟过程较慢,timeout 的出现比较频繁
    • 通过编译开源网络检测库 iputilsC 代码的方式对 traceroute 进行了套接字发送 ICMP 报文模拟,可以明显提高检测速度
  • 关于代码 ping 的过程信息
    • 开启一个 AsyncTask,在 doInBackground 方法中开始解析,这个是入口。
    • 添加头部信息,主要包括:开始诊断 + 输出关于应用、机器、网络诊断的基本信息 + 输出本地网络环境信息
    • tcp 三次握手操作
      • 开始执行链接,这里有两个重要信息。一个是 ip 集合,另一个是 InetAddress 数组,遍历【长度是 ip 集合 length】,然后执行请求
      • 创建 socketAddress,有两个参数,一个是 ip,一个是端口号 80,然后 for 循环执行 socket 请求
      • 在执行 socket 请求的时候,如果有监听到超时 SocketTimeoutException 异常则记录数据,如果有异常则记录数据
      • 当出现发生 timeOut,则尝试加长连接时间,注意连续两次连接超时,停止后续测试。连续两次出现 IO 异常,停止后续测试
      • 当然只要有一次完整执行成功的流程,那么则记录三次握手操作成功
    • 诊断 ping 信息, 同步过程。这个主要是直接通过 ping 命令监测网络
      • 创建一个 NetPing 对象,设置每次 ping 发送数据包的个数为 4 个
      • 然后 ping 本机 ip 地址,ping 本地网观 ip 地址,ping 本地 dns。这个 ping 的指令是啥?这个主要是用 java 中的 Runtime 执行指令……
    • 开始诊断 traceRoute
      • 先调用原生 jni 代码,调用 jni c 函数执行 traceroute 过程。如果发生了异常,再调用 java 代码执行操作……
      • 然后通过 ping 命令模拟执行 traceroute 的过程,比如:ping -c 1 -t 1 www.jianshu.com
      • 如果成功获得 trace:IP,则再次发送 ping 命令获取 ping 的时间

4.2 ping 使用截图

image

05.该库如何使用

5.1 崩溃库

  • 如何引入该库
      //崩溃重启库
      implementation 'cn.yc:ToolLib:1.2.3'
      //网络拦截日志库
      implementation 'cn.yc:NetLib:1.0.1'
      //GitHub 代码
      https://github.com/yangchong211/YCAndroidTool
    
  • 初始化代码如下所示。建议在 Application 中初始化……

      CrashHandler.getInstance().init(this, new CrashListener() {
          /**
           * 重启 app
           */
          @Override
          public void againStartApp() {
              CrashToolUtils.reStartApp1(App.this,1000);
              //CrashToolUtils.reStartApp2(App.this,1000, MainActivity.class);
              //CrashToolUtils.reStartApp3(AppManager.getAppManager().currentActivity());
          }
    
          /**
           * 自定义上传 crash,支持开发者上传自己捕获的 crash 数据
           * @param ex                        ex
           */
          @Override
          public void recordException(Throwable ex) {
              //自定义上传 crash,支持开发者上传自己捕获的 crash 数据
              //StatService.recordException(getApplication(), ex);
          }
      });
    
  • 关于重启 App 的操作有三种方式 api
      //开启一个新的服务 KillSelfService,用来重启本 APP【使用 handler 延迟】
      CrashToolUtils.reStartApp1(App.this,1000);
      //用来重启本 APP[使用闹钟,整体重启,临时数据清空(推荐)]
      CrashToolUtils.reStartApp2(App.this,1000, MainActivity.class);
      //检索获取项目中 LauncherActivity,然后设置该 activity 的 flag 和 component 启动 app【推荐】
      CrashToolUtils.reStartApp3(AppManager.getAppManager().currentActivity());
    
  • 关于获取崩溃目录 api
      //崩溃文件存储路径:/storage/emulated/0/Android/data/你的包名/cache/crashLogs
      //崩溃页面截图存储路径:/storage/emulated/0/Android/data/你的包名/cache/crashPics
      String crashLogPath = ToolFileUtils.getCrashLogPath(this);
      String crashPicPath = ToolFileUtils.getCrashPicPath(this);
    
  • 关于崩溃日志记录
    • 日志记录路径:/storage/emulated/0/Android/data/你的包名/cache/crashLogs
    • 日志文件命名:V1.0_2020-09-02_09:05:01_java.lang.NullPointerException.txt【版本+日期+异常】
  • 关于跳转错误日志 list 列表页面
    • 跳转日志列表页面如下所示,这里调用一行代码即可。点击该页面 list 条目即可进入详情
      CrashToolUtils.startCrashListActivity(this);
      
  • 那么如何获取所有崩溃日志的 list 呢。建议放到子线程中处理!!

      List<File> fileList = ToolFileUtils.getCrashFileList(this);
    
      //如果是要自己拿到这些文件,建议根据时间来排个序
      //排序
      Collections.sort(fileList, new Comparator<File>() {
          @Override
          public int compare(File file01, File file02) {
              try {
                  //根据修改时间排序
                  long lastModified01 = file01.lastModified();
                  long lastModified02 = file02.lastModified();
                  if (lastModified01 > lastModified02) {
                      return -1;
                  } else {
                      return 1;
                  }
              } catch (Exception e) {
                  return 1;
              }
          }
      });
    
  • 如何删除单个文件操作
      //返回 true 表示删除成功
      boolean isDelete = ToolFileUtils.deleteFile(file.getPath());
    
  • 如何删除所有的文件。建议放到子线程中处理!!
      File fileCrash = new File(ToolFileUtils.getCrashLogPath(CrashListActivity.this));
      ToolFileUtils.deleteAllFiles(fileCrash);
    
  • 如何获取崩溃文件中的内容
      //获取内容
      String crashContent = ToolFileUtils.readFile2String(filePath);
    
  • 还有一些关于其他的 api,如下。这个主要是方便测试同学或者产品,避免开发不承认那种偶发性崩溃 bug……
      //拷贝文件,两个参数分别是源文件,还有目标文件
      boolean copy = ToolFileUtils.copyFile(srcFile, destFile);
      //分享文件。这个是调用原生的分享
      CrashLibUtils.shareFile(CrashDetailsActivity.this, destFile);
      //截图崩溃然后保存到相册。截图---> 创建截图存储文件路径---> 保存图片【图片质量,缩放比还有采样率压缩】
      final Bitmap bitmap = ScreenShotsUtils.measureSize(this,view);
      String crashPicPath = ToolFileUtils.getCrashPicPath(CrashDetailsActivity.this) + "/crash_pic_" + System.currentTimeMillis() + ".jpg";
      boolean saveBitmap = CrashLibUtils.saveBitmap(CrashDetailsActivity.this, bitmap, crashPicPath);
    
  • 异常恢复原理
    • 第一种方式,开启一个新的服务 KillSelfService,用来重启本 APP。
        CrashToolUtils.reStartApp1(App.this,1000);
      
    • 第二种方式,使用闹钟延时,然后重启 app
        CrashToolUtils.reStartApp2(App.this,1000, MainActivity.class);
      
    • 第三种方式,检索获取项目中 LauncherActivity,然后设置该 activity 的 flag 和 component 启动 app
        CrashToolUtils.reStartApp3(AppManager.getAppManager().currentActivity());
      
    • 关于 app 启动方式详细介绍

5.2 如何网络拦截

  • 如下所示
      new OkHttpClient.Builder()
          //配置工厂监听器。主要是计算网络过程消耗时间
          .eventListenerFactory(NetworkListener.get())
          //主要是处理拦截请求,响应等信息
          .addNetworkInterceptor(new NetworkInterceptor())
          .build()
    
  • 如何开启悬浮按钮
      //建议只在 debug 环境下显示,点击去网络拦截列表页面查看网络请求数据
      NetworkTool.getInstance().setFloat(getApplication());
    
  • 该库目的
    • 做成悬浮全局按钮,点击按钮可以查看该 activity 页面请求接口,可以查看请求几个接口,以及接口请求到响应消耗流量
    • 方便查看网络请求流程,比如 dns 解析时间,请求时间,响应时间
    • 方便测试查看请求数据,方便抓包。可以复制 request,respond,body 等内容。也可以截图

5.3 如何使用 ping

  • 直接创建一个 ping,需要传递一个网址 url
      _netDiagnoService = new NetDiagnoService(getContext(), getContext().getPackageName()
              , versionName, userId, deviceId, host, this);
      _netDiagnoService.execute();
    
  • 如何取消 ping
      if (_netDiagnoService!=null){
          _netDiagnoService.cancel(true);
          _netDiagnoService = null;
      }
    
  • 或者直接停止 ping。停止线程允许,并把对象设置成 null
      _netDiagnoService.stopNetDialogsis();
    
  • 关于监听

      /**
       * 诊断结束,输出全部日志记录
       * @param log                       log 日志输出
       */
      @Override
      public void OnNetDiagnoFinished(String log) {
          setText(log);
      }
    
      /**
       * 监控网络诊断过程中的日志输出
       * @param log                       log 日志输出
       */
      @Override
      public void OnNetDiagnoUpdated(String log) {
          showInfo += log;
          setText(showInfo);
      }
    

06.后续的需求说明

  • 可能不兼容
    • 该库尚未通过多进程应用程序进行测试。如果您使用这种配置进行测试,请提供反馈!
    • 如果您的应用程序初始化监听或错误活动崩溃,则有可能进入无限重启循环(在大多数情况下,库会对此进行检查,但在极少数情况下可能会发生)。
    • 修复 Android P 反射限制导致的 Activity 生命周期异常无法 finish Activity 问题。某些机型还是不兼容……
  • App 崩溃收集信息说明
    • 收集崩溃时的基本信息
      • 进程(前台进程还是后台进程)
      • 线程(是否是 UI 线程)
      • 崩溃堆栈(具体崩溃在系统的代码,还是我们自己的代码里面)
      • 崩溃堆栈类型(Java 崩溃、Native 崩溃 or ANR)
    • 收集崩溃时的系统信息
      • 机型、系统、厂商、CPU、ABI、Linux 版本等。(寻找共性)
      • Logcat。(包括应用、系统的运行日志,其中会记录 App 运行的一些基本情况)
    • 收集崩溃时的内存信息(OOM、ANR、虚拟内存耗尽等,很多崩溃都跟内存有直接关系)
      • 系统剩余内存。(系统可用内存很小 – 低于 MemTotal 的 10%时,OOM、大量 GC、系统频繁自杀拉起等问题都非常容易出现)
      • 虚拟内存(但是很多类似 OOM、tgkill 等问题都是虚拟内存不足导致的)
      • 应用使用内存(得出应用本身内存的占用大小和分布)
      • 线程数
    • 收集崩溃时的应用信息
      • 崩溃场景(崩溃发生在哪个 Activity 或 Fragment,发生在哪个业务中)
      • 关键操作路径(记录关键的用户操作路径,这对我们复现崩溃会有比较大的帮助)
      • 其他自定义信息(不同应用关心的重点不一样。例如运行时间、是否加载了补丁、是否是全新安装或升级等)

07.异常栈轨迹原理

  • Android 发生异常为何崩溃
    • 一旦线程出现抛出异常,并且我们没有捕捉的情况下,JVM 将调用 Thread 中的 dispatchUncaughtException 方法把异常传递给线程的未捕获异常处理器。发现最后会使用到 Thread.getDefaultUncaughtExceptionHandler()
    • 既然 Android 遇到异常会发生崩溃,然后找一些哪里用到设置 setDefaultUncaughtExceptionHandler,即可定位到 RuntimeInit 类。
    • 具体可以找到 RuntimeInit 类,然后在找到 KillApplicationHandler 类。首先看该类的入口 main 方法--->commonInit()--->,然后接着往下走,找到 setDefaultUncaughtExceptionHandler 代码。当出现异常是 try-catch,并且在 finally 中直接 kill 杀死 app 操作。
    • 详细可以看:Android 项目崩溃分析
  • 崩溃后异常堆栈链是如何形成的

08.部分问题反馈

  • 该异常捕获实效了是什么情况?
    • Thread.setDefaultUncaughtExceptionHandler(handler) 方法如果被多次调用的话,会以最后一次传递的 handler 为准,所以如果用了第三方的统计模块,可能会出现失灵的情况。对于这种情况,在设置默认 hander 之前,可以先通过 getDefaultUncaughtExceptionHandler() 方法获取并保留旧的 hander,然后在默认 handler 的 uncaughtException 方法中调用其他 handler 的 uncaughtException 方法,保证都会收到异常信息。
  • 关于上传日志介绍
    • 设置该异常初始化后,在进入全局异常时系统就提示尽快收集信息,进程将被结束,因此不可以在此时做网络上传崩溃信息。可以在此时将错误日志写入到 file 文件或者 sp 中。
    • 比如:通过 SharedPreferences 将错误日志的路径写入配置文件中,在启动的时候先检测该配置文件是否有错误日志信息,如果有则读取文件,然后实现日志上传。上传完成后删除该 sp 文件……
  • 使用 looper 可以拦截崩溃和 anr 吗
    • 可以实现拦截 UI 线程的崩溃,耗时性能监控。但是也并不能够拦截所有的异常。如果在 Activity 的 onCreate 出现崩溃,导致 Activity 创建失败,那么就会显示黑屏。
    • fork 出 app 进程后,在 ActivityThread 中,在 main 方法的最后调用了 Looper.loop(),在这个方法中处理主线程的任务调度,一旦执行完这个方法就意味着 APP 被退出了。
    • 果主线程发生了异常,就会退出循环,意味着 APP 崩溃,所以我们我们需要进行 try-catch,避免 APP 退出,再启动一个 Looper.loop() 去执行主线程任务,就不会退出。
    • looper 拦截崩溃或者 anr,存在一个巨大的问题,就是按钮点不动或者无反应。有可能导致出现其他问题……这个需要慎重使用

09.其他内容说明

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools