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 崩溃重启实践
- 05.Native异常捕获
- 5.1 native崩溃日志组成
- 5.2 捕获native异常
- 5.3 注册信号处理函数
- 5.4 获取native异常信息
- 5.5 写native异常到文件
- 5.6 异常抛到Java层
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 导致卡死)
- Java 崩溃(比如
- 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); } } }
- 得出结论如下所示
- 其实在
fork
出app
进程的时候,系统已经为app
设置了一个异常处理,并且最终崩溃后会直接导致执行该handler
的finally
方法最后杀死app直接退出app。
- 其实在
- 如何自己捕获App异常
- 如果你要自己处理,你可以自己实现
Thread.UncaughtExceptionHandler
。而调用setDefaultUncaughtExceptionHandler
多次,最后一次会覆盖之前的。
- 如果你要自己处理,你可以自己实现
2.2.2 崩溃日志的记录
- 在
KillApplicationHandler
类中的uncaughtException
方法- 可以看到
ActivityManager.getService().handleApplicationCrash
被调用,那么这个是用来做什么的呢? - ActivityManager.getService().handleApplicationCrash-->ActivityManagerService.handleApplicationCrash-->handleApplicationCrashInner方法
- 可以看到
- 从下面可以看出,若传入
app
为null
时,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
是殊途同归呢,因为无论是crash
、native_crash
、ANR
或是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();
- 1、首先发生crash所在进程,在
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() { @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
,然后在默认handler
的uncaughtException
方法中调用其他handler
的uncaughtException
方法,保证都会收到异常信息。
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() 函数时,函数调用栈的情况。
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
- 截图如下所示
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
05.Native异常捕获
5.1 Native崩溃日志组成
- 设备指纹
- 用于记录设备相关信息
- Revision
- Revision 指的是硬件,而不是软件。通常情况下不使用 revision,但使用 revision 有助于您自动忽略由不良硬件导致的已知错误
- ABI
- ABI 是 arm、arm64、mips、mips64、x86 或 x86-64 之一,记录该值主要用于后续对信息分析时区分使用什么工具链
- 进程相关信息
- 包括进程号、进程名称
- 错误信号
- 接收的信号 (这里是SIGABRT) 以及有关如何接收该信号的更多信息 (这里是SI_TKILL)
- 中断消息
- 并非所有崩溃问题都会有中止消息行,但发生中止时,会出现该消息行。这是从此 pid/tid 的最后一行严重 logcat 输出中自动收集而来的,而在有意中止的情况下,这可以解释该程序自行终止的原因
- 寄存器信息
- 展示所有寄存器中的内容,这些内容的有用程度取决于确切的崩溃问题
- 调用栈
- crash函数调用栈
5.2 捕获native异常
- Linux信号机制如何处理崩溃
- 所有的崩溃都是编程错误或者硬件错误相关的,系统遇到不可恢复的错误时会触发崩溃机制让程序退出,如除零、段地址错误等。
- 异常发生时,CPU通过异常中断的方式,触发异常处理流程,不同的处理器有不同的异常中断类型和中断处理方式。
- Linux把这些中断处理统一为信号量,可以注册信号量向量进行处理。
- 既然 Android 系统也是基于 Linux 的,那么我们可以利用 Linux 的信号机制进行异常捕获。
- 在Linux系统中,当发生异常时,内核会以信号机制通知进程,收到信号后,进程可以有以下三种处理方案:
- 忽略信号。对该信号不做任何处理,就像未发生过一样;
- 使用系统默认操作。这种缺省操作对大部分的信号是使进程终止;
- 指定处理函数处理。类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数;
- 那么以什么方处理native异常
- 需要自己指定信号处理函数捕捉信号
5.3 注册信号处理函数
- 如何使用指定的信号处理函数
- 进程通过 sigaction() 函数来指定对某个信号的处理行为。
#include <signal.h> int sigaction(int sig, const struct sigaction* new_action, struct sigaction* old_action);
5.4 获取native异常信息
5.5 写native异常到文件
5.6 异常抛到Java层
其他内容说明
- 关于附带的demo:https://github.com/yangchong211/YCAndroidTool
参考博客
- native crash收集流程分析
- https://www.jianshu.com/p/77a53d7b642e
- https://juejin.cn/post/6854573218171191309#heading-5
- 鹰眼Android平台崩溃监控实践
- https://mp.weixin.qq.com/s/YSrXx_oTJkp0kRDF6XvzdQ