01.崩溃捕获设计实践

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 {@Overridepublic 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 {@Overridepublic void run() {try {//1.一直循环地读peerFd文件,若发生存在,则进入consumeNativeCrashDatawhile (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 {@Overridepublic 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崩溃流程图
    • image
  • 崩溃流程叙述
    • 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 死亡通知的原理,其流程图如下所示:
    • image
  • 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() {@Overridepublic void uncaughtException(Thread t, Throwable e) {System.out.println("所有线程异常都会被捕获,捕获所有线程:"+t.toString() + " throwable : " + e.getMessage());}
    });
    
  • uncaughtExceptionHandler:为单个线程设置一个属于线程自己的uncaughtExceptionHandler,辖范围比较小。
    //为单个线程设置一个属于线程自己的uncaughtExceptionHandler,捕获单个线程异常。设置后,线程可以完全控制它对未捕获到的异常作出响应的处理。
    thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {@Overridepublic 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 referenceat 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() 函数时,函数调用栈的情况。
    • image

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);}
    }
    
  • 崩溃监听核心流程图
    • image

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
      
  • 截图如下所示
    • imageimage
    • imageimage

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetcode 567. 字符串的排列(滑动窗口-java)

滑动窗口 字符串的排列滑动窗口代码演示进阶优化版 上期经典 字符串的排列 难度 -中等 leetcode567. 字符串的排列 给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。 换句…

ios开发 swift5 苹果系统自带的图标 SF Symbols

文章目录 1.官网app的下载和使用2.使用代码 1.官网app的下载和使用 苹果官网网址:SF Symbols 通过上面的网址可以下载dmg, 安装到自己的mac上 貌似下面这样不能展示出动画,还是要使用动画的代码 .bounce.up.byLayer2.使用代码 UIKit UIImage(system…

PDF可以修改内容吗?有什么注意的事项?

PDF是一种跨平台的电子文档格式,可以在各种设备上轻松阅读和共享。许多人喜欢将文档转换为PDF格式以确保格式的一致性和易读性。但是,PDF文件一般被认为是“只读”文件,即无法编辑。那么,PDF文件是否可以修改呢? 答案是…

循环结构(个人学习笔记黑马学习)

while循环语句 在屏幕中打印0~9这十个数字 #include <iostream> using namespace std;int main() {int i 0;while (i < 10) {cout << i << endl;i;}system("pause");return 0; } 练习案例: 猜数字 案例描述:系统随机生成一个1到100之间的数字&…

Kotlin的Lambda闭包语法

Lambda 表达式是一种在现代编程语言中常见的特性&#xff0c;它可以用来创建匿名函数或代码块&#xff0c;使得将函数作为参数传递、简化代码以及实现函数式编程范式变得更加便捷。Lambda 表达式在函数式编程语言中得到广泛应用&#xff0c;也在诸如 Java 8 和 Kotlin 等主流编…

微软用 18 万行 Rust 重写了 Windows 内核

微软正在使用 Rust 编程语言重写其核心 Windows 库。 5 月 11 日——Azure 首席技术官 Mark Russinovich 表示&#xff0c;最新的 Windows 11 Insider Preview 版本是第一个包含内存安全编程语言 Rust 的版本。 “如果你参加了 Win11 Insider 环&#xff0c;你将在 Windows 内…

WebGL矩阵变换库

目录 矩阵变换库&#xff1a; Matrix4对象所支持的方法和属性如表所示&#xff1a; 方法属性规范&#xff1a; 虽然平移、旋转、缩放等变换操作都可以用一个44的矩阵表示&#xff0c;但是在写WebGL程序的时候&#xff0c;手动计算每个矩阵很耗费时间。为了简化编程&#xf…

Docker 轻量级可视化工具Portainer

1. 是什么 Portainer 是一款轻量级的应用&#xff0c;它提供了图形化界面&#xff0c;用于方便地管理Docker环境&#xff0c;包括单机环境和集群环境。 2. 安装 2.1 官网 https://www.protainer.io/ https://docs.portainer.io/ce-2.9/start/install/server/docker/linux 2.2 …

基于 vue2 发布 npm包

背景&#xff1a;组件化开发需要&#xff0c;走了一遍发布npm包的过程&#xff0c;采用很简单的模式实现包的发布流程&#xff0c;记录如下。 项目参考&#xff1a;基于vue的时间播放器组件&#xff0c;并发布到npm_timeplay.js_xmy_wh的博客-CSDN博客 1、项目初始化 首先&a…

基于React实现日历组件详细教程

前言 日历组件是常见的日期时间相关的组件&#xff0c;围绕日历组件设计师做出过各种尝试&#xff0c;展示的形式也是五花八门。但是对于前端开发者来讲&#xff0c;主要我们能够掌握核心思路&#xff0c;不管多么奇葩的设计我们都能够把它做出来。 本文将详细分析如何渲染一…

【LeetCode】227. 基本计算器 II

227. 基本计算器 II&#xff08;中等&#xff09; 方法&#xff1a;双栈解法 思路 我们可以使用两个栈 nums 和 ops 。 nums &#xff1a; 存放所有的数字ops &#xff1a;存放所有的数字以外的操作 然后从前往后做&#xff0c;对遍历到的字符做分情况讨论&#xff1a; 空格 …

springboot 基于JAVA的动漫周边商城的设计与实现64n21

动漫周边商城分为二个模块&#xff0c;分别是管理员功能模块和用户功能模块。管理员功能模块包括&#xff1a;文章资讯、文章类型、动漫活动、动漫商品功能&#xff0c;用户功能模块包括&#xff1a;文章资讯、动漫活动、动漫商品、购物车&#xff0c;传统的管理方式对时间、地…

2023-08-28 LeetCode每日一题(插入区间)

2023-08-28每日一题 一、题目编号 57. 插入区间二、题目链接 点击跳转到题目位置 三、题目描述 给你一个 无重叠的 &#xff0c;按照区间起始端点排序的区间列表。 在列表中插入一个新的区间&#xff0c;你需要确保列表中的区间仍然有序且不重叠&#xff08;如果有必要的…

暴力递归转动态规划(二)

上一篇已经简单的介绍了暴力递归如何转动态规划&#xff0c;如果在暴力递归的过程中发现子过程中有重复解的情况&#xff0c;则证明这个暴力递归可以转化成动态规划。 这篇帖子会继续暴力递归转化动态规划的练习&#xff0c;这道题有点难度。 题目 给定一个整型数组arr[]&…

element-ui 弹窗里面嵌套弹窗,解决第二个弹窗被遮罩层掩盖无法显示的问题

当我们在 element-ui 中使用弹窗嵌套弹窗时&#xff0c;会出现第二个弹窗打开时被一个遮罩层挡着&#xff0c;就像下面这样&#xff1a; 下面提供两种解决方案 &#xff1a; 一、第一种方案 我们查询element-ui 官网可以发现 el-dialog 有这样几个属性&#xff1a; 具体使用就…

hadoop 学习:mapreduce 入门案例三:顾客信息与订单信息相关联(联表)

这里的知识点在于如何合并两张表&#xff0c;事实上这种业务场景我们很熟悉了&#xff0c;这就是我们在学习 MySQL 的时候接触到的内连接&#xff0c;左连接&#xff0c;而现在我们要学习 mapreduce 中的做法 这里我们可以选择在 map 阶段和reduce阶段去做 数据&#xff1a; …

java版工程项目管理系统源码+系统管理+系统设置+项目管理+合同管理+二次开发

工程项目各模块及其功能点清单 一、系统管理 1、数据字典&#xff1a;实现对数据字典标签的增删改查操作 2、编码管理&#xff1a;实现对系统编码的增删改查操作 3、用户管理&#xff1a;管理和查看用户角色 4、菜单管理&#xff1a;实现对系统菜单的增删改查操…

人工智能会成为人类的威胁吗?马斯克、扎克伯格、比尔·盖茨出席

根据消息人士透露&#xff0c;此次人工智能洞察论坛将是一次历史性的聚会&#xff0c;吸引了来自科技界的许多重量级人物。与会者们将共同探讨人工智能在科技行业和社会发展中的巨大潜力以及可能带来的挑战。 埃隆马斯克&#xff0c;特斯拉和SpaceX的首席执行官&#xff0c;一直…

如何提高视频清晰度?视频调整清晰度操作方法

现在很多小伙伴通过制作短视频发布到一些短视频平台上记录生活&#xff0c;分享趣事。但制作的视频有些比较模糊&#xff0c;做视频的小伙伴应该都知道&#xff0c;视频画质模糊不清&#xff0c;会严重影响观众的观看体验。 通过研究&#xff0c;总结了以下几点严重影响的点 …

Android12之ABuffer数据处理(三十四)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药. 更多原创,欢迎关注:Android…