前言#
AndroidCrash基础知识
研究的对象是APP进程
研究的问题主体是 APP为什么会Crash
研究的路线是:从APP进程被Crash的第一行代码开始溯源,Java->JVM->Android平台->Android系统->Devices
,直至我们最后能看懂的地方。
研究的工具是:
- Source Insight 4.0
- UML
- Logcat
- Logger
- MtkLog
- SysTrace
- TraceView
- Profiler
- Leakcanary
- Blockcanary
- Buggly
- 友盟
- 自建APM
研究的参考资料是:
- Java JRE SRC
- Android Framwork SRC
- 工具对应的文档
主要介绍两个方面:
- Java(JVM)级别的Crash现象以及原理
- AppCrash容灾方案1.0
- 异常捕获原理及最佳实践
graph LR Head(Crash知识分享大纲)-->preKno preKno(前置知识)-->Crash(Crash) Crash-->c1(定义) Crash-->c2(分类) preKno-->Exception(异常) Exception-->e1(定义) Exception-->e2(分类) Head-->code(原理分析) code-->co1(Java)-->co11(JavaThrowable) co1-->co12(JavaException) code-->co2(JVM)-->co22(方法加载原理) co2-->co23(类加载原理) co2-->co24(JVM异常原理) Head-->CrashDemo(CrashDemo案例) CrashDemo-->cr1(主线程异常) CrashDemo-->cr2(子线程异常) CrashDemo-->cr3(未捕获异常处理器) CrashDemo-->cr4(默认未捕获异常处理器) CrashDemo-->cr6(演示APP停止运行Dialog) CrashDemo-->cr7(演示APP黑屏闪退且无Dialog) CrashDemo-->cr5(try-catch-exception机器码) Head-->CrashProject(Crash工程化实践) CrashProject-->cp1(线下工具) CrashProject-->cp2(线上工具) CrashProject-->cp3(编码实践)
Crash知识分享第一期#
3个现象#
有这么3种现象(不算ANR)
现象名称 | 原因 | |
---|---|---|
App已停止运行 | RuntimeException OutOfMemoryError |
|
App闪退 | 黑屏并回到Launcher首页 | StackOverflowError |
App屡次停止运行 | 多次异常 | |
(补充)App没有响应 | 参考唐将《ANR知识分享》 |
Demo
根据Crash源头不同,Crash级别可以分为以下4类
- JVM:Throwable类问题
- OS平台:内存、文件流、中断等
- 硬件设备:路由器、宽带、设备重启、摄像头、内存卡、磁盘等
- 物理环境:水、温度等
Java级别Crash原理#
定义#
什么是Java级别的Crash?
JavaAPP
或AndroidAPP
运行过程中,发生了Throwable
类问题,被JVM识别捕获JVM
主动操作导致程序退出,而非Android平台OS、硬件设备、物理环境导致程序退出
关于Java级别Crash主要研究哪些问题?
研究对象:Java最小任务单位Thread
研究场景:JavaAPP程序、AndroidAPP程序
场景区别:Java虚拟机、Android虚拟机
研究问题:
- Crash杀死了谁?连带杀死了谁?-Thread\ThreadGroup\Process
- 谁Crash了谁?谁杀死了APP?- Runtime
- 停止运行Dialog是谁调用弹出的?-
- Exception抛出流程?-Exception对象是x创建的,第一个抛出者是xx
- Logcat打印流程?-每一行ExceptionLog是xx、xx打印的
研究对象Thread#
主要回答一个问题:Crash杀死的是谁?——运行APP的最小单位是Thread(Process),所以杀死的是当前Thread
Thread基础知识#
Thread[4]#
代码位置:jdk\src\java\lang\Thread.java
1 | /** |
JVM执行Thread对象时,处理异常流程为:
- JVM在执行当前
Thread
对象时遇到了异常并进行抛出,如果JVM发现开发者并未对该异常进行捕获,则JVM会调用Thread
的dispatchUncaughtException
方法将Exception对象传递给Thread
持有的UncaughtExceptionHandler
- 如果
Thread
持有的UncaughtExceptionHandler
不为空,则交由Thread
对象持有的UncaughtExceptionHandler 对象处理; - 如果
Thread
持有的UncaughtExceptionHandler
为空,则交给ThreadGroup
来处理Thread
所抛出Exception
对象
ThreadGroup[4]#
代码位置:jdk\src\java\lang\ThreadGroup.java
1 |
|
ThreadGroup
处理uncaughtException
的流程很简洁
- 默认情况,先将Exception对象交给
父ThreadGroup
处理 - 判断Thread是否持有
defaultUncaughtExceptionHandler
默认异常处理器 - 如果有默认异常处理器,则交由其处理
- 如果没有异常处理器,则将错误信息输出到
System.err
总结一下Exception处理顺序,假设某个Thread抛出Exception
消费Exception的顺序为
- Thread
UncaughtExceptionHandler
- ThreadGroup
- ParentThread
DefaultUncaughtExceptionHandler
UncaughtExceptionHandler
主线程#
JavaAPP#
1 | public class CrashTest { |
异常堆栈
1 | Exception in thread "iron man is on the main thread" java.lang.ArithmeticException: / by zero |
可以看到线程名是主线程main
AndroidAPP#
1 | /** |
异常堆栈
1 | 2021-04-25 14:33:38.372 4707-4707/com.work.demotest E/AndroidRuntime: FATAL EXCEPTION: iron man is on the android main thread |
子线程#
JavaAPP#
1 | /** |
异常堆栈
1 | Exception in thread "iron man is on the child thread" java.lang.ArithmeticException: / by zero |
AndroidAPP#
1 | /** |
异常堆栈
1 | 2021-04-25 14:35:25.580 4775-4811/com.work.demotest E/AndroidRuntime: FATAL EXCEPTION: iron man is on the android chilid thread |
JavaAPP与AndroidAPP默认异常堆栈区别
命令行默认异常堆栈 | JavaAPP | AndroidAPP |
---|---|---|
Process | :negative_squared_cross_mark: | :heavy_check_mark: |
Thread | :heavy_check_mark: | :heavy_check_mark: |
Exception | :heavy_check_mark: | :heavy_check_mark: |
Android主线程与子线程异常堆栈区别
Android异常堆栈 | Process | Thread | view | ActivityThread | ZygoteInit |
---|---|---|---|---|---|
ChildThread | :heavy_check_mark: | :heavy_check_mark: | :negative_squared_cross_mark: | :negative_squared_cross_mark: | :negative_squared_cross_mark: |
MainThread | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
疑问:Android主线程异常,为什么抛出如此多的信息?为什么有这种区别?
答:因为AndroidAPP的主线程所做的任务位于ActivityThread.main
中,在main
方法中做了比子线程更多的任务。
研究对象Runtime<span class=”hint–top hint–error hint–medium hint–rounded hint–bounce” aria-label=” Runtime.java#
“>[3]
主要回答一个问题,谁杀死了APP?
JavaRuntime#
代码位置:jre/java/lang/Runtime
调用者:暂时理解为虚拟机(没有查到准确的触发者)
代码用途:杀死JavaAPP、运行JavaAPP
1 | /** |
AndroidRuntime<span class=”hint–top hint–error hint–medium hint–rounded hint–bounce” aria-label=” RuntimeInit.java (androidxref.com)#
“>[2]
代码位置: /frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
调用者:暂时理解为虚拟机(没有查到准确的触发者)
代码用途:运行AndroidAPP、杀死AndroidAPP、打印AndroidAPP运行过程的Log
1 | /** |
总结下AppRuntime处理流程
main
调用了commonInit
commonInit
中设置了setUncaughtExceptionPreHandler
、setDefaultUncaughtExceptionHandler
- 未捕获异常处理器
setUncaughtExceptionPreHandler
设置了LoggingHandler
- 默认未捕获异常处理器
setDefaultUncaughtExceptionHandler
设置了KillApplicationHandler
- 未捕获异常处理器
观察步骤2-b 这也就是为什么在App中设置默认异常捕获器可以防止APP闪退的原因
Runtime研究的启示有以下几点:
- 无论是JavaAPP或是AndroidAPP,只要为Thread设置了未捕获异常处理器,就可以降低Crash的几率
- 在未捕获异常处理器中可以做很多任务:异常信息持久化,上报日志,单个线程设置处理器,APP容灾处理
- 主线程的默认未捕获异常处理器是
KillApplicationHandler
,默认会杀死当前进程
如果我们追溯代码,可以看到停止运行的弹出框的数据格式为ApplicationErrorReport#CrashInfo
代码位置:frameworks/base/core/java/android/app/ApplicationErrorReport.java
代码用途:创建Crash数据对象,保存命令行打印的错误信息
内部类名称 | 用途 |
---|---|
CrashInfo | 描述Crash信息 |
AnrInfo | 描述ANR信息 |
BatteryInfo | 描述电池信息 |
代码位置ActivityManagerService
代码用途:调出AppErrors的方法停止运行弹出框Dialog
1 | public class ActivityManagerService extends IActivityManager.Stub{ |
代码位置AppErrors
代码用途:Dialog工具类,如创建停止运行弹出框Dialog
1 | class AppErrors { |
代码位置AppErrorDialog
代码用途:弹出框Dialog
1 | final class AppErrorDialog extends BaseErrorDialog implements View.OnClickListener { |
Java未捕获异常处理器#
CrashTest
测试类,类中定义了内部类ExceptionHandler
1 | public class CrashTest { |
主线程#
1 |
|
异常日志
1 | iron man is on the main thread,captured a exception |
子线程#
1 | /** |
异常日志
1 | iron man is on the child thread,captured a exception |
Android未捕获异常处理器#
主线程#
定义Application
1 | <application |
App
中定义未捕获异常处理器,未开启默认未捕获异常处理
1 | public class App extends Application { |
问题:setUncaughtExceptionHandler是否能高枕无忧?保证不Crash?
答:不能
测试主线程运行时异常#
依然用Thread#子线程#AndroidAPP的代码演示
1 | /** |
日志堆栈
1 | 2021-04-26 10:36:24.678 7136-7136/com.work.demotest D/ExceptionHandler: uncaughtExceptioniron man is on the android main threadhas captured a exception |
可以看到主线程的Exception
被未捕获异常处理器
处理了,并且跳转到了默认页面NoneUeCrashActivity
,无异常堆栈。
测试子线程运行时异常#
1 | /** |
日志堆栈
1 | 2021-04-26 10:39:37.421 7409-7439/com.work.demotest E/AndroidRuntime: FATAL EXCEPTION: iron man is on the android chilid thread |
可以看到异常堆栈,并且弹出了停止运行弹窗。
通过设置主、子线程的setDefaultUncaughtExceptionHandler
和setUncaughtExceptionHandler
,记录是否显示停止运行Dialog的结果如下:
是否显示停止运行Dialog |
主线程运行时异常 | 子线程运行时异常 |
---|---|---|
主线程设置setUncaughtExceptionHandler | :negative_squared_cross_mark: | :heavy_check_mark: |
主线程设置setDefaultUncaughtExceptionHandler | :negative_squared_cross_mark: | :negative_squared_cross_mark: |
子线程设置setUncaughtExceptionHandler | :negative_squared_cross_mark: | :negative_squared_cross_mark: |
子线程设置setDefaultUncaughtExceptionHandler | :negative_squared_cross_mark: | :negative_squared_cross_mark: |
可以发现:
- 主线程仅设置setUncaughtExceptionHandler,如果主线程抛出异常,则不会闪退,也不会弹出Dialog;如果子线程抛出异常,则App会闪退弹出Dialog;
- 主线程设置setDefaultUncaughtExceptionHandler,如果子线程抛出异常,则App不会闪退,也不会显示Dialog
- 子线程设置setUncaughtExceptionHandler或子线程设置setDefaultUncaughtExceptionHandler都不会闪退,也不会弹出Dialog
问题:Thread.setDefaultUncaughtExceptionHandler能解决什么问题?
答:
RuntimeInit中为APP设置的俩异未捕获常处理器Thread.setUncaughtExceptionPreHandler(loggingHandler);
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
未捕获异常处理器最佳实践#
制定容灾策略#
在当前页面对MVVM、MVP或MVC各个层级做以下操作:退出、恢复、重试、缓存
跳转到业务首页,如登录流程某一步异常跳转到登录首页、视频通话页异常跳转到聊天详情页
显示错误页或错误弹窗,如遇到OOM,Dialog显示
内存不足,请优化内存后再试
、遇到StackOverFlow,Dialog显示程序异常,进入克服中心反馈
保存错误数据,定时上报服务器
…
主线程和子线程作区分#
- 子线程设置未捕获异常处理器,执行自己的容灾恢复逻辑
1 | /** |
App
中主线程设置未捕获异常处理器,覆盖默认未捕获异常处理
1 | Thread.currentThread().setName("android app "); |
Crash容灾1.0#
常见做法 |
---|
开源库CockRoach |
开源库DefenseCrashApplication |
方法块进行try catch finally、throws 、throw new |
对UI主线程设置未捕获异常处理器、默认未捕获异常处理器 |
对子线程设置未捕获异常处理器、默认未捕获异常处理器 |
研究对象虚拟机#
主要回答一个问题:谁调用App的main方法,并持有当前App主线程对象?
Java主线程是的任务入口是Java类#main
Anroidd主线程的任务入口是ActivityThread#main
AndroidJVM类似,仅以JavaJVM举例
JavaJVM#
编写一段try-catch代码,如下
1 | public class ExceptionCode { |
编译为class文件
1 | javac ExceptionCode.class |
编译为字节码文件
1 | javap -c ExceptionCode.class |
查看字节码文件
1 | Compiled from "ExceptionCode.java" |
Exception Table 异常表#
异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下
- from 可能发生异常的起始点
- to 可能发生异常的结束点
- target 上述from和to之前发生异常后的异常处理者的位置
- type 异常处理者处理的异常的类信息,包括具体的包名类名
这张表足够回答许多常见异常捕获类的问题,不要急,这些问题将在《Crash知识分享第二期》展开讨论。
在这一步我们仅需要知道,在编译阶段结束的时候,finally里面的部分会被复制两份至try语句的结尾
和catch语句的结尾
.
异常表定义了main方法拥有3种执行分支
- 0-4之间,发生了指定类型为Exception的异常,则会跳转到语句15
- 接下来15-24之间,会执行打印
字符串Exception
、字符串finally
的机器码
- 接下来15-24之间,会执行打印
- 0-4之间,无法伦发生什么异常,都会跳转到语句15
- 接下来15-24之间,会执行打印
字符串Exception
、字符串finally
的机器码
- 接下来15-24之间,会执行打印
- 15-24之间,无论发生什么异常,都会跳转到语句35
- 接下来24-29之间,会执行打印
字符串finally
的机器码
- 接下来24-29之间,会执行打印
那么异常表用在什么时候呢[1]
答案是异常发生的时候,当一个异常发生时
1.JVM会在当前出现异常的方法中,查找异常表,是否有合适的处理者来处理
2.如果当前方法异常表不为空,并且异常符合处理者的from和to节点,并且type也匹配,则JVM调用位于target的调用者来处理。
3.如果上一条未找到合理的处理者,则继续查找异常表中的剩余条目
4.如果当前方法的异常表无法处理,则向上查找(弹栈处理)刚刚调用该方法的调用处,并重复上面的操作。
5.如果所有的栈帧被弹出,仍然没有处理,则抛给当前的Thread,Thread则会终止。
6.如果当前Thread为最后一个非守护线程,且未处理异常,则会导致JVM终止运行。
以上就是JVM处理异常的一些机制。
Java级别Crash小结#
从Java级别Crash这一章的内容,我们了解到以下两点:
Crash原理
JVM在执行机器码生成的方法表时,如果发现了Exception,会按照机器码既定的语句执行,此时有两种情况
- 开发者编写了捕获语句:catch、finally
- 无捕获语句处理,则继续throws,直至抛给JVM堆中的Thread对象
- 此时如果Thread为子线程,则会判断是否设置了未捕获异常处理器
- 若设置了处理器,则进入处理器处理,不会退出程序
- 若没有设置处理器,Android平台会进入KillApplicationHandler的逻辑,该逻辑有以下几种
- 弹出“App已停止运行”的Dialog
- 弹出“App频繁停止运行”的Dialog
- 直接退出程序
Crash容灾方案(初版)
- 制定容灾策略
- 主线程和子线程作区分处理
遗留了一些问题
- e.printStackTrace()的开销有多大?
- JVM创建Exception的开销有多少?
- catch的最佳实践
- finally一定会执行吗?
- 1.详解JVM如何处理异常 - 技术小黑屋 (droidyue.com) ↩
- 2. RuntimeInit.java (androidxref.com) ↩
- 3. Runtime.java ↩
- 4.[Java Platform SE 8](file:///E:/developedoc/jdk-8u281-docs-all/docs/api/index.html) ↩