关于android:01崩溃捕获设计实践方案

38次阅读

共计 17374 个字符,预计需要花费 44 分钟才能阅读完成。

01. 解体捕捉设计实际计划

目录介绍
  • 01. 整体介绍概述

    • 1.1 我的项目背景介绍
    • 1.2 遇到问题
    • 1.3 根底概念介绍
    • 1.4 设计指标
  • 02.App 解体流程

    • 2.1 为何解体推出 App
    • 2.2 Java 解体流程
    • 2.3 Native 解体流程
    • 2.4 解体日志解决
    • 2.5 最初推出 App
    • 2.6 解体流程叙述
    • 2.7 Binder 死亡告诉
  • 03. 解体解决入口

    • 3.1 Java 解决异样入口
    • 3.2 异样解决罕用 api
    • 3.3 注意事项阐明
    • 3.4 JVM 解决异样入口
    • 3.5 了解异样栈轨迹链
    • 3.6 JVM 如何实现异样
  • 04. 解体捕捉思路

    • 4.1 实现解体监听
    • 4.2 解决捕捉异样
    • 4.3 实现雷同异样次数统计
    • 4.4 解体日志收集
    • 4.5 捕捉指定线程异样
    • 4.6 日志可视化查看
    • 4.7 日志发送邮箱
    • 4.8 解体重启实际

01. 整体介绍概述

1.1 我的项目背景介绍
  • Android的稳定性是 Android 性能的一个重要指标,它也是 App 品质构建体系中最根本和最要害的一环。

    • 如果利用常常解体率,或者要害性能不可用,那显然会对咱们的留存产生重大影响。
1.2 遇到问题
  • Crash率多少算优良呢?

    • 在明确了指标之后,咱们能力正确认识咱们的工作到底有什么作用。升高解体率到咱们的指标……
  • 解体率如何掂量

    • 解体率 UV = 产生解体的 UV / 启动 UV
    • 衡量标准:解体率小于 3 /1000 为失常,3/10000 为优良
1.3 根底概念介绍
  • 解体现场是“第一案发现场”,它保留着很多有价值的线索。

    • 接下来具体来看看在解体现场,确认重点,内存 & 线程需特地留神,很多解体都是因为它们使用不当造成的。如何去剖析日志
  • 确认重大水平

    • 如果一时半会解决不了,那么是否先止损,采纳降级策略。延期修复,如果是非要解决,那么解决完后即通过灰度测试发版,及时跟进问题。
  • 解体根本信息

    • Java 解体(比方 NullPPointerException 是空指针,OutOfMemoryError 是资源有余)
    • Native 解体(比拟常见的是有 SIGSEGV 和 SIGABRT)
    • ANR(先看看主线程的堆栈,是否是因为锁期待导致。接着看看 ANR 日志中 iowait、CPU、GC、system server 等信息,进一步确定是 I/O 问题,或是 CPU 竞争问题,还是因为大量 GC 导致卡死)
  • Logcat 日志

    • Logcat 中咱们能够看到过后零碎的一些行为跟手机的状态,当从一条解体日志中无奈看出问题的起因,或者得不到有用信息时,不要放弃,倡议查看雷同崩溃点下的更多解体日志。
  • 查找共性(机型、零碎、ROM、厂商、ABI)

    • 机型、零碎、ROM、厂商、ABI,这些采集到的零碎信息都能够作为维度聚合,共性问题例如是不是因为装置了 Xposed,是不是只呈现在 x86 的手机,是不是只有三星这款机型,是不是只在 Android 8.0 的零碎上。
  • 复现问题

    • 尽量去找到复现问题的链路,不便排查问题。有些 bug 如果找不到,那么思考是否上传 info 日志,通过技术埋点去排查解体链路问题。
1.4 设计指标
  • 可能精确将解体日志写到本地文件

    • 可能捕捉到解体日志,而后把它通过 io 流写入到 file 文件中。写入的解体信息,带有残缺的异样堆栈链信息,还有一些根底的手机和 App 属性。
  • 可能无效计算雷同解体的次数

    • 比方针对同一段代码的类型转化异样java.lang.NumberFormatException: For input string: "12.3",如果呈现屡次,须要统计到具体的次数。
  • 可能可视化展现解体日志信息

    • 这一块,次要是可能读到解体日志门路,拿到所有的文件。而后通过可视化界面展现进去,不便查看!
  • 可能将解体信息文件转发分享

    • 可能将解体 file 文件分享到微信,QQ 或者钉钉这类社交 App,不便测试童鞋转发给开发。MonitorFileLib

02.App 解体流程

2.1 为何解体推出 App
  • 线程中抛出异样当前的解决逻辑

    • 一旦线程呈现抛出异样,并且在没有捕获的状况下,JVM将调用 Thread 中的 dispatchUncaughtException 办法把异样传递给线程的未捕捉异样处理器。
  • 找到 Android 源码中解决异样捕捉入口

    • 既然 Android 遇到异样会产生解体,而后找一些哪里用到设置 setDefaultUncaughtExceptionHandler,即可定位到RuntimeInit 类。
    • 即在这个外面设置异样捕捉 KillApplicationHandler,产生异样之后,会调用handleApplicationCrash 打印输出解体 crash 信息,最初会杀死利用app
2.2 解决解体流程
2.2.1 解体的大略流程
  • 而后看一下 RuntimeInit 类,因为是 java 代码,所以首先找 main 办法入口。代码如下所示

    public static final void main(String[] argv) {commonInit();
    }
  • 而后再来看一下 commonInit() 办法,看看外面做了什么操作?

    • 能够发现这里调用了 setDefaultUncaughtExceptionHandler 办法,设置了自定义的 Handler

      protected static final void commonInit() {Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
      }
  • 接着看一下 KillApplicationHandler 类,能够发现该类实现了 Thread.UncaughtExceptionHandler 接口

    • 这个就是杀死 app 逻辑具体的代码。能够看到当出现异常的时候,在 finally 中会退出过程操作。

      private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
      @Override
      public void uncaughtException(Thread t, Throwable e) {
          try {ActivityManager.getService().handleApplicationCrash(mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
          } finally {Process.killProcess(Process.myPid());
              System.exit(10);
          }
      }
      }
  • 得出结论如下所示

    • 其实在 forkapp过程的时候,零碎曾经为 app 设置了一个异样解决,并且最终解体后会间接导致执行该 handlerfinally办法最初杀死 app 间接退出 app。
  • 如何本人捕捉 App 异样

    • 如果你要本人解决,你能够本人实现 Thread.UncaughtExceptionHandler。而调用setDefaultUncaughtExceptionHandler 屡次,最初一次会笼罩之前的。
2.2.2 解体日志的记录
  • KillApplicationHandler 类中的 uncaughtException 办法

    • 能够看到 ActivityManager.getService().handleApplicationCrash 被调用,那么这个是用来做什么的呢?
    • ActivityManager.getService().handleApplicationCrash–>ActivityManagerService.handleApplicationCrash–>handleApplicationCrashInner 办法
  • 从上面能够看出, 若传入 appnull时,processName就设置为system_server

    public void handleApplicationCrash(IBinder app, ApplicationErrorReport.ParcelableCrashInfo crashInfo) {ProcessRecord r = findAppProcess(app, "Crash");
        final String processName = app == null ? "system_server" : (r == null ? "unknown" : r.processName);
        handleApplicationCrashInner("crash", r, processName, crashInfo);
    }
  • 而后接着看一下 handleApplicationCrashInner 办法做了什么。调用 addErrorToDropBox 将利用crash,进行封装输入

    void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName,
            ApplicationErrorReport.CrashInfo crashInfo) {addErrorToDropBox(eventType, r, processName, null, null, null, null, null, crashInfo);
        mAppErrors.crashApplication(r, crashInfo);
    }
  • 解体日志封装流程如下所示

    ActivityManagerService#handleApplicationCrash(),在这个办法里解决解体日志信息
    ActivityManagerService#findAppProcess(),这个是依据 binder 去找对应的 crash 的 ProcessRecord 对象
    ActivityManagerService#handleApplicationCrashInner(),这个办法很要害
    ActivityManagerService#addErrorToDropBox(),这个就是将 crash,anr,装到盒子里。这个次要在上面会说到
    ActivityManagerService#appendDropBoxProcessHeaders,这个办法是拼接 app 的过程,pid,package 包名等等

2.3 Native 解体流程
  • Native 解体监控入口流程

    SystemServer#main(),在 fork 出 system_server 过程后执行 main 办法,而后创立该对象并且执行 run 办法做初始化各种服务逻辑
    SystemServer#run(),在这个线程 run 办法中,调用 startOtherServices 开启各种服务逻辑
    SystemServer#startOtherServices(),在这个办法里,是零碎 system_server 过程开启泛滥服务,比方 IMS 输出事件服务,NMS 告诉栏服务等
    ActivityManagerService#startObservingNativeCrashes(),在这个类中创立 NativeCrashListener 去监控 native 解体

  • native_crash,顾名思义,就是 native 层产生的 crash。其实他是通过一个NativeCrashListener 线程去监控的。

    final class NativeCrashListener extends Thread {
        @Override
        public void run() {
            try {
                //1. 始终循环地读 peerFd 文件, 若产生存在, 则进入 consumeNativeCrashData
                while (true) {
                    try {if (peerFd != null) {
                            //2. 进入 native crash 数据处理流程
                            consumeNativeCrashData(peerFd);
                        }
                    } 
                }
            }
        }
    
        void consumeNativeCrashData(FileDescriptor fd) {
            try {
                    //3. 启动 NativeCrashReporter 作为上报谬误的新线程
                    final String reportString = new String(os.toByteArray(), "UTF-8");
                    (new NativeCrashReporter(pr, signal, reportString)).start();} catch (Exception e) {}}
    }
  • 上报 native_crash 的线程 –>NativeCrashReporter:

    class NativeCrashReporter extends Thread {
        @Override
        public void run() {
            try {
                //1. 包装解体信息
                CrashInfo ci = new CrashInfo();
                //2. 转到 ams 中解决, 跟一般 crash 统一, 只是类型不一样
                mAm.handleApplicationCrashInner("native_crash", mApp, mApp.processName, ci);
            } catch (Exception e) {}}
    }
  • native crash跟到这里就完结了,前面的流程就是跟 application crash 一样,都会走到 addErrorToDropBox 中。
2.4 解体日志解决
  • 为什么说 addErrorToDropBox 是必由之路呢,因为无论是 crashnative_crashANR 或是wtf,最终都是来到这里,交由它去解决。

    public void addErrorToDropBox(……) {
        // 只有这几种类型的谬误, 才会进行上传
        final boolean shouldReport = ("anr".equals(eventType)
                || "crash".equals(eventType)
                || "native_crash".equals(eventType)
                || "watchdog".equals(eventType));
        //1. 如果 DropBoxManager 没有初始化, 或不是要上传的类型, 则间接返回
        if (dbox == null || !dbox.isTagEnabled(dropboxTag)&& !shouldReport)
            return;
        //2. 增加一些头部 log 信息 
        //3. 增加解体过程和界面的信息
        //4. 增加过程的状态到 dropbox 中
        //5. 将 dataFile 文件定入 dropbox 中, 个别只有 anr 时, 会将 traces 文件通过该参数传递进来者, 其余类型都不传.
        //6. 如果是 crash 类型, 会传入 crashInfo, 此时将其写入 dropbox 中
        if (shouldReport) {synchronized (mErrorListenerLock) {
                try {
                    //7. 要害, 在这里能够增加一个 application error 的接口,用来实现应用层接管解体信息
                    mIApplicationErrorListener.onError(fEventType,
                            packageName, fProcessName, subject, dropboxTag + "-" + uuid, crashInfo);
                } 
            }
        }
    }
2.5 最初推出 App
  • 推出 App 的形式常见的有哪些?思考一下,零碎是采纳那种形式推出 App,为什么?

    • 第一种:在根页面,调用 finish 间接推出 App 的首页,Activity会调用onDestroy。这种状况过程其实是未杀死的状况,
    • 第二种:在根页面,调用 moveTaskToBack 推出 App,这种相似 home 键作用,Activity是调用 onStop 回到后盾。
    • 第三种:finish 所有的 activity 推出 App,这种状况下,过程可能存活。
    • 第四种:间接调用 killProcess 杀死过程,而后在调用 System.exit 推出程序。这种形式是彻底杀死过程,比拟粗犷【零碎就是这种】。
  • App 常见敌对的推出形式

    • 杀死过程:先回退到桌面,而后 finish 掉所有 activity 页面,而后在杀死过程和推出程序。能够防止闪一下……
2.6 解体流程叙述
  • App 解体流程图

  • 解体流程叙述

    • 1、首先产生 crash 所在过程,在 RuntimeInit 创立之初便筹备好了 defaultUncaughtHandler,用来来解决 Uncaught Exception,并输入以后 crash 根本信息;
    • 2、调用以后过程中的AMP.handleApplicationCrash,通过 binder ipc 机制,传递到 system_server 过程;
    • 3、接下来,进入 system_server 过程,调用 binder 服务端执行AMS.handleApplicationCrash
    • 4、从 AMS.findAppProcess 查找到指标过程的 ProcessRecord 对象;而后调用AMS.handleApplicationCrashInner,并将过程 crash 信息输入到目录 /data/system/dropbox;
    • 5、执行ActivityManagerService#addErrorToDropBox(),这个就是将 crash,anr,装到盒子里。这个次要在上面会说到;
    • 6、回到 RuntimeInit 解决解体 finally 中,执行杀死过程操作,当 crash 过程被杀,通过 binder 死亡告诉,告知 system_server 过程来执行 appDiedLocked();
2.7 Binder 死亡告诉
  • 还须要理解下 binder 死亡告诉的原理,其流程图如下所示:

  • binder 死亡告诉原理

    • 因为 Crash 过程中领有一个 Binder 服务端 ApplicationThread,而利用过程在创立过程调用 attachApplicationLocked(),从而 attach 到 system_server 过程,在 system_server 过程内有一个 ApplicationThreadProxy,这是绝对应的 Binder 客户端。
    • 当 Binder 服务端 ApplicationThread 所在过程 (即 Crash 过程) 挂掉后,则 Binder 客户端能收到相应的死亡告诉,从而进入 binderDied 流程。

03. 解体解决入口

3.1 Java 解决异样入口
  • UncaughtExceptionHandler接口,官网介绍为:

    @FunctionalInterface
    public interface UncaughtExceptionHandler {void uncaughtException(Thread t, Throwable e);
    }
    • Interface for handlers invoked when a Thread abruptly terminates due to an uncaught exception.
    • When a thread is about to terminate due to an uncaught exception the Java Virtual Machine will query the thread for its UncaughtExceptionHandler using getUncaughtExceptionHandler() and will invoke the handler’s uncaughtException method, passing the thread and the exception as arguments. If a thread has not had its UncaughtExceptionHandler explicitly set, then its ThreadGroup object acts as its UncaughtExceptionHandler. If the ThreadGroup object has no special requirements for dealing with the exception, it can forward the invocation to the default uncaught exception handler.
  • 翻译后大略的意思是

    • UncaughtExceptionHandler接口用于解决因为一个未捕捉的异样而导致一个线程忽然终止问题。
    • 当一个线程因为一个未捕捉的异样行将终止时,Java 虚拟机将通过调用 getUncaughtExceptionHandler() 函数去查问该线程的 UncaughtExceptionHandler 并调用处理器的 uncaughtException 办法将线程及异样信息通过参数的模式传递进去。如果一个线程没有明确设置一个 UncaughtExceptionHandler,那么 ThreadGroup 对象将会代替 UncaughtExceptionHandler 实现该行为。如果 ThreadGroup 没有明确指定解决该异样,ThreadGroup 将转发给默认的解决未捕捉的异样的处理器。
  • 线程呈现未捕捉异样后,JVM 将调用 Thread 中的 dispatchUncaughtException 办法把异样传递给线程的未捕捉异样处理器。

    public final void dispatchUncaughtException(Throwable e) {getUncaughtExceptionHandler().uncaughtException(this, e);
    }
    public UncaughtExceptionHandler getUncaughtExceptionHandler() {return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group;}
3.2 异样解决罕用 api
3.2.1 设置 uncaughtExceptionPreHandler
  • Thread中存在两个UncaughtExceptionHandler

    • 一个是动态的defaultUncaughtExceptionHandler,另一个是非动态uncaughtExceptionHandler

      private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
      private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
  • defaultUncaughtExceptionHandler: 设置一个动态的默认的UncaughtExceptionHandler

    • 来自所有线程中的 Exception 在抛出并且未捕捉的状况下,都会从此路过。过程 fork 的时候设置的就是这个动态的defaultUncaughtExceptionHandler,管辖范畴为整个过程。

      Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
      @Override
      public void uncaughtException(Thread t, Throwable e) {System.out.println("所有线程异样都会被捕捉,捕捉所有线程:"+t.toString() + "throwable :" + e.getMessage());
      }
      });
  • uncaughtExceptionHandler: 为单个线程设置一个属于线程本人的uncaughtExceptionHandler,辖范畴比拟小。

    // 为单个线程设置一个属于线程本人的 uncaughtExceptionHandler,捕捉单个线程异样。设置后,线程能够齐全管制它对未捕捉到的异样作出响应的解决。thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(Thread t, Throwable e) {System.out.println("捕捉单个线程:"+t.toString() + "throwable :" + e.getMessage());
        }
    });
3.2.2 没有设置 uncaughtExceptionPreHandler
  • 没有设置 uncaughtExceptionHandler 怎么办?

    • 如果没有设置uncaughtExceptionHandler,将应用线程所在的线程组来解决这个未捕捉异样。
    • 线程组 ThreadGroup 实现了UncaughtExceptionHandler,所以能够用来解决未捕捉异样。ThreadGroup 类定义:

      private ThreadGroup group;
      // 能够发现 ThreadGroup 类是集成 Thread.UncaughtExceptionHandler 接口的
      class ThreadGroup implements Thread.UncaughtExceptionHandler{}
  • 而后看一下 ThreadGroup 中实现 uncaughtException(Thread t, Throwable e) 办法,代码如下

    • 默认状况下,线程组解决未捕捉异样的逻辑是,首先将异样音讯告诉给父线程组,而后尝试利用一个默认的 defaultUncaughtExceptionHandler 来解决异样,
    • 如果没有默认的异样处理器则将错误信息输入到System.err。也就是 JVM 提供给咱们设置每个线程的具体的未捕捉异样处理器,也提供了设置默认异样处理器的办法。

      public void uncaughtException(Thread t, Throwable e) {if (parent != null) {parent.uncaughtException(t, e);
      } else {
          // 返回线程因为未捕捉到异样而忽然终止时调用的默认处理程序。如果返回值为 null,则没有默认处理程序。Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler();
          if (ueh != null) {ueh.uncaughtException(t, e);
          } 
      }
      }
3.3 注意事项阐明
  • 难道要为每一个线程创立 UncaughtExceptionHandler 吗?

    • 应用程序通常都会创立很多线程,如果为每一个线程都设置一次 UncaughtExceptionHandler 未免太过麻烦。
    • 既然呈现未解决异样后 JVM 最终都会调 getDefaultUncaughtExceptionHandler(),那么咱们能够在利用启动时设置一个默认的未捕捉异样处理器。
    • 即调用 Thread.setDefaultUncaughtExceptionHandler(handler) 就能够。
  • setDefaultUncaughtExceptionHandler被调用屡次如何了解?

    • Thread.setDefaultUncaughtExceptionHandler(handler)办法如果被屡次调用的话,会以最初一次传递的 handler 为准。
    • 所以如果用了第三方的统计模块,可能会呈现失灵的状况。
    • 对于这种状况,在设置默认 handler 之前,能够先通过 getDefaultUncaughtExceptionHandler() 办法获取并保留旧的 handler,而后在默认handleruncaughtException办法中调用其余 handleruncaughtException办法,保障都会收到异样信息。
3.4 JVM 解决异样入口
  • 思考一下:JVM 拿到异样之后是如何将捕捉的异样回调到 java 层的 uncaughtException 办法。

    • Hotspot 虚拟机源码的 thread.cpp 中的 JavaThread::exit 办法发现了这样的一段代码,并且还给出了正文:

      if (HAS_PENDING_EXCEPTION) {ResourceMark rm(this);
      jio_fprintf(defaultStream::error_stream(),
          "\nException: %s thrown from the UncaughtExceptionHandler"
          "in thread \"%s\"\n",
          pending_exception()->klass()->external_name(),
          get_thread_name());
      CLEAR_PENDING_EXCEPTION;
      }
  • 在线程调用 exit 退出时

    • 如果有未捕捉的异样,则会调用 Thread.dispatchUncaughtException 办法。这个则是 java 层解决异样的入口!
3.5 了解异样栈轨迹链
  • 来看一个简略的解体日志,如下所示:

    • 那么这个解体日志,是怎么造成的解体异样链的?简略来说,在办法调用链路中,存在栈治理。

      Process: com.yc.ycandroidtool, PID: 16060
      java.lang.NullPointerException: Attempt to invoke virtual method 'void android.app.Activity.finish()' on a null object reference
      at com.com.yc.appmonitor.crash.CrashTestActivity.onClick(CrashTestActivity.java:48)
      at android.view.View.performClick(View.java:7187)
      at android.view.View.performClickInternal(View.java:7164)
      at android.view.View.access$3500(View.java:813)
      at android.view.View$PerformClick.run(View.java:27626)
      at android.os.Handler.handleCallback(Handler.java:883)
      at android.os.Handler.dispatchMessage(Handler.java:100)
      at android.os.Looper.loop(Looper.java:230)
      at android.app.ActivityThread.main(ActivityThread.java:7742)
      at java.lang.reflect.Method.invoke(Native Method)
      at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1034)
  • 在这个解体日志,能够发现

    • ZygoteInit.main —-> RuntimeInit —-> ActivityThread.main —-> Handler.dispatchMessage —> View.performClick —> CrashTestActivity.onClick
    • 察看可知,这个解体信息则是记录着 app 从启动到解体中的流程日志。
  • StackTraceElement此类在 java.lang 包下

    • public final class StackTraceElement extends Object implements Serializable
    • 堆栈跟踪元素,它由 Throwable.getStackTrace() 返回。每个元素示意独自的一个【堆栈帧】。
    • 所有的堆栈帧(堆栈顶部的那个堆栈帧除外)都示意一个【办法调用】。堆栈顶部的帧示意【生成堆栈跟踪的执行点】。通常,这是创立对应于堆栈跟踪的 throwable 的点。
3.6 JVM 如何实现异样
  • 那么思考一下,jvm是如何结构 Throwable 异样的呢?
  • 异样实例的结构非常低廉

    • 因为在结构异样实例时,JVM 须要生成该异样的栈轨迹,该操作逐个拜访以后线程的 Java 栈桢,并且记录下各种调试信息,包含栈桢所指向办法的名字、办法所在的类名以及办法在源代码中的地位等信息。
  • JVM 捕捉异样须要异样表

    • 每个办法都有一个异样表,异样表中的每一个条目都代表一个异样处理器,并且由 from、to、target 指针及其异样类型所形成。form-to 其实就是 try 块,而 target 就是 catch 的起始地位。
    • 当程序触发异样时,JVM 会检测触发异样的字节码的索引值落到哪个异样表的 from-to 范畴内,而后再判断异样类型是否匹配,匹配就开始执行 target 处字节码解决该异样。
  • 最初是 finally 代码块的编译

    • finally 代码块肯定会运行的(除非虚拟机退出了)。那么它是如何实现的呢?其实是一个比拟笨的方法,以后 JVM 的做法是,复制 finally 代码块的内容,别离放在所有可能的执行门路的进口中。
  • 如何了解 Java 函数调用栈桢呢

    • 操作系统给每个线程调配了一块独立的内存空间,这块内存被组织成“栈”这种构造, 用来存储函数调用时的长期变量。每进入一个函数,就会将长期变量作为一个栈帧入栈,当被调用函数执行实现,返回之后,将这个函数对应的栈帧出栈。

      int main() {
       int a = 1; 
       int ret = 0;
       int res = 0;
       ret = add(3, 5);
       res = a + ret;
       printf("%d", res);
       reuturn 0;
      }
      
      int add(int x, int y) {
       int sum = 0;
       sum = x + y;
       return sum;
      }
    • 从代码中咱们能够看出,main() 函数调用了 add() 函数,获取计算结果,并且与长期变量 a 相加,最初打印 res 的值。
    • 为了让你清晰地看到这个过程对应的函数栈里出栈、入栈的操作,我画了一张图。图中显示的是,在执行到 add() 函数时,函数调用栈的状况。

04. 解体监听思路

4.1 实现解体监听
  • ThreadHandler这个类就是实现了 UncaughtExceptionHandler 这个接口。handler将会报告线程终止和不明起因异样这个状况。

    public class ThreadHandler implements Thread.UncaughtExceptionHandler {
        private Thread.UncaughtExceptionHandler mDefaultHandler;
        public void init(Application ctx) {mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
            Thread.setDefaultUncaughtExceptionHandler(this);
        }
    }
  • 解体监听外围流程图

4.2 解决捕捉异样
  • 当出现异常的时候,最终会将异样散发到 uncaughtException 这个回调办法中。解决捕捉异样相干操作,就是在这个办法中解决

    @Override
    public void uncaughtException(Thread t, Throwable e) {// 解决业务,能够拿到线程和异样 throwable 对象,解析异样操作}
4.3 实现雷同异样次数统计
  • 大略的思路如下所示

    • 每一次产生解体时,拿到异样 Throwable,而后获取它的堆栈信息,转化为字符串后再 md5 一下失去一个 key。
    • 每一次存储的时候,获取之前的【如果之前没有则是 0】次数加一
  • 留神问题点:要害是怎么判断两个解体是同一个?

    • 举一个例子:Integer.parseInt(“12.3”) 和 Integer.parseInt(“12.4”) 它们都是 NumberFormatException 异样,但却是不同的。获取堆栈再 md5 一下即可保障 key 惟一
4.4 解体日志收集
4.4.1 收集解体信息
  • 从解体的根本信息,能够对解体有初步的判断。

    • 过程名、线程名。解体的过程是前台过程还是后盾过程,解体是不是产生在 UI 线程。
    • 解体堆栈和类型。解体是属于 Java 解体、Native 解体,还是 ANR,对于不同类型的解体咱们关注的点也不太一样。特地须要看解体堆栈的栈顶,看具体解体在零碎的代码,还是咱们本人的代码外面。
  • 收集解体时的零碎信息

    • 机型、零碎、厂商、CPU、ABI、Linux 版本等。(寻找共性)
    • Logcat。(包含利用、零碎的运行日志,其中会记录 App 运行的一些根本状况)
    • 设施状态:是否 root、是否是模拟器。一些问题是由 Xposed 或多开软件造成,对这部分问题咱们要区别对待。
  • 收集解体时的内存信息(OOM、ANR、虚拟内存耗尽等,很多解体都跟内存有间接关系)

    • 零碎残余内存。(零碎可用内存很小 – 低于 MemTotal 的 10% 时,OOM、大量 GC、零碎频繁他杀拉起等问题都非常容易呈现)
    • 虚拟内存(然而很多相似 OOM、tgkill 等问题都是虚拟内存有余导致的)
    • 利用应用内存(得出利用自身内存的占用大小和散布)
  • 资源信息

    • 有的时候咱们会发现利用堆内存和设施内存都十分短缺,还是会呈现内存调配失败的状况,这跟资源透露可能有比拟大的关系。
    • 文件句柄 fd。个别单个过程容许关上的最大文件句柄个数为 1024。然而如果文件句柄超过 800 个就比拟危险,须要将所有的 fd 以及对应的文件名输入到日志中,进一步排查是否呈现了有文件或者线程的透露
    • 线程数。一个线程可能就占 2MB 的虚拟内存,过多的线程会对虚拟内存和文件句柄带来压力。依据我的教训来说,如果线程数超过 400 个就比拟危险。须要将所有的线程 id 以及对应的线程名输入到日志中,进一步排查是否呈现了线程相干的问题。
  • 收集解体时的利用信息

    • 解体场景(解体产生在哪个 Activity 或 Fragment,产生在哪个业务中)
    • 要害操作门路(记录要害的用户操作门路,这对咱们复现解体会有比拟大的帮忙)
    • 其余自定义信息(不同利用关怀的重点不一样。例如运行工夫、是否加载了补丁、是否是全新装置或降级等)
4.4.2 收集日志具体阐明
  • Logcat。这里包含利用、零碎的运行日志。

    • 因为零碎权限问题,获取到的 Logcat 可能只蕴含与以后 App 相干的。其中零碎的 event logcat 会记录 App 运行的一些根本状况,记录在文件 /system/etc/event-log-tags 中。

      system logcat:
      10-25 17:13:47.788 21430 21430 D dalvikvm: Trying to load lib ...
      event logcat:
      10-25 17:13:47.788 21430 21430 I am_on_resume_called: 生命周期
      10-25 17:13:47.788 21430 21430 I am_low_memory: 零碎内存不足
      10-25 17:13:47.788 21430 21430 I am_destroy_activity: 销毁 Activty
      10-25 17:13:47.888 21430 21430 I am_anr: ANR 以及起因
      10-25 17:13:47.888 21430 21430 I am_kill: APP 被杀以及起因
    • 机型、零碎、厂商、CPU、ABI、Linux 版本等。–> 寻找共性
    • 设施状态:是否 root、是否是模拟器。一些问题是由 Xposed 或多开软件造成,对这部分问题咱们要区别对待。
  • 内存信息

    • OOM、ANR、虚拟内存耗尽等,很多解体都跟内存有间接关系。
    • 零碎残余内存。对于零碎内存状态,能够间接读取文件 /proc/meminfo。当零碎可用内存很小(低于 MemTotal 的 10%)时,OOM、大量 GC、零碎频繁他杀拉起等问题都非常容易呈现。
    • 利用应用内存。包含 Java 内存、RSS(Resident Set Size)、PSS(Proportional Set Size),咱们能够得出利用自身内存的占用大小和散布。PSS 和 RSS 通过 /proc/self/smap 计算,能够进一步失去例如 apk、dex、so 等更加具体的分类统计。
    • 虚拟内存。虚拟内存能够通过 /proc/self/status 失去,通过 /proc/self/maps 文件能够失去具体的散布状况。有时候咱们个别不太器重虚拟内存,然而很多相似 OOM、tgkill 等问题都是虚拟内存有余导致的。

      opened files count 812:
      0 -> /dev/null
      1 -> /dev/log/main4
      2 -> /dev/binder
      3 -> /data/data/com.crash.sample/files/test.config
    • 线程数。以后线程数大小能够通过下面的 status 文件失去,一个线程可能就占 2MB 的虚拟内存,过多的线程会对虚拟内存和文件句柄带来压力。依据我的教训来说,如果线程数超过 400 个就比拟危险。须要将所有的线程 id 以及对应的线程名输入到日志中,进一步排查是否呈现了线程相干的问题。

      threads count 412:
      1820 com.sample.crashsdk
      1844 ReferenceQueueD
      1869 FinalizerDaemon
  • 截图如下所示

4.6 日志可视化查看
  • 能够通过该工具查看缓存文件

    • 疾速查看 data/data/ 包名 目录下的缓存文件。
    • 疾速查看 /sdcard/Android/data/ 包名 下存储文件。
  • 一键接入该工具

    • FileExplorerActivity.startActivity(MainActivity.this);
    • 开源我的项目地址:https://github.com/yangchong211/YCAndroidTool
  • 可视化界面展现

4.7 日志发送邮箱
  • 发送邮件分为两种:

    • 调用零碎的发邮件性能发送邮件
    • 应用特定的邮箱密码发送邮件
  • 发送优先必备操作

    • 要应用 JavaMail 的三个 jar 包:activation.jar;additionnal.jar;mail.jar
  • 发送流程如下所示

    • 设置发送服务器;设置发送账户和明码;设置发送显示的名称, 主题, 内容和附件;设置接收者地址;发送邮件给接收者
4.8 解体重启实际
  • 第一种形式,开启一个新的服务 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,具体的 Demo 能够看:

    • https://github.com/yangchong211/YCAppTool/tree/master/CommonLib/AppRestartLIb

其余内容阐明

  • 对于附带的 demo:https://github.com/yangchong211/YCAndroidTool

正文完
 0