稳定性问题分析20211108
1. JE 问题分析
Java Exception的分析方法相对要简单很多,java堆栈会保留出错的调用栈,能精确到代码指定的行号。如果问题容易复现,可以直接用logcat命令复现并保存日志。如果是已经发生的低概率问题,机器现场还在的话,可以通过导出data/system/dropbox目录下的日志文件。通常是data_app_crash、system_app_crash、system_server_crash开头,以txt为后缀。通过分析日志堆栈可以快速定位到出错的代码。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ix6H1rt0-1636330330493)(C:\Users\20247969\AppData\Roaming\Typora\typora-user-images\image-20211101104436301.png)]
Java的异常可以分为两类:Checked Exception和UnChecked Exception。所有RuntimeException类及其子类的实例被称为Runt ime异常,即UnChecked Exception,不是RuntimeException类及其子类的异常实例则被称为Checked Exception。
1.1 Checked Exception
Checked异常又称为编译时异常,即在编译阶段被处理的异常。编译器会强制程序处理所有的Checked异常,也就是用try…catch显式的捕获并处理,因为Java认为这类异常都是可以被处理(修复)的。在Java API文档中,方法说明时,都会添加是否throw某个exception,这个exception就是Checked异常。如果没有try…catch这个异常,则编译出错,错误提示类似于“Unhandled
exception type xxxxx”。
该类异常捕获的流程是:
- 执行try块中的代码出现异常,系统会自动生成一个异常对象,并将该异常对象提交给Java运行环境,这个就是异常抛出(throw)阶段;
- 当Java运行环境收到异常对象时,会寻找最近的能够处理该异常对象的catch块,找到之后把该异常对象交给catch块处理,这个就是异常捕获(catch)阶段。
Checked异常一般是不引起Android App Crash的,注意是“一般”,这里之所以介绍Checked异常,有两个原因:
- 形成系统的了解,更好地对比理解UnCheckedException;
- 对于一些Checked Exception,虽然我们在程序里面已经捕获并处理了,但是如果能同时将该异常收集并发送到后台,将有助于提升App的健壮性。比如修改代码逻辑回避该异常,或者捕获后采用更好的方法去处理该异常。至于应该收集哪些Checked Exception,则取决于App的业务逻辑和开发者的经验了。
1.2 UnChecked Exception
UnChecked异常又称为运行时异常,即Runtime-Exception,最常见的莫过于NullPointerException。UnChecked异常发生时,由于没有相应的try…catch处理该异常对象,所以Java运行环境将会终止,程序将退出,也就是我们所说的Crash。当然,你可能会说,那我们把这些异常也try…catch住不就行了。理论上确实是可以的,但有两点会导致这种方案不可行:
无法将所有的代码都加上try…catch,这样对代码的效率和可读性将是毁灭性的;
UnChecked异常通常都是较为严重的异常,或者说已经破坏了运行环境的。比如内存地址,即使我们try…catch住了,也不能明确知道如何处理该异常,才能保证程序接下来的运行是正确的。
没有try…catch住的异常,即Uncaught异常,都会导致应用程序崩溃。那么面对崩溃,我们是否可以做些什么呢?比如程序退出前,弹出个性化对话框,而不是默认的强制关闭对话框,或者弹出一个提示框安慰一下用户,甚至重启应用程序等。
其实Java提供了一个接口给我们,可以完成这些,这就是UncaughtExceptionHandler,该接口含有一个纯虚函数:public abstract void uncaughtException (Thread thread, Throwableex)。
Uncaught异常发生时会终止线程,此时,系统便会通知UncaughtExceptionHandler,告诉它被终止的线程以及对应的异常,然后便会调用uncaughtException函数。如果该handler没有被显式设置,则会调用对应线程组的默认handler。如果我们要捕获该异常,必须实现我们自己的handler,并通过以下函数进行设置:
public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)
实现自定义的handler,只需要继承UncaughtExceptionHandler该接口,并实现uncaughtException方法即可。
static class MyCrashHandler implements UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread thread, final Throwable throwable) {
// Deal this exception
}
}
在任何线程中,都可以通过setDefaultUncaughtExceptionHandler来设置handler,但在Android应用程序中,全局的Application和Activity、Service都同属于UI主线程,线程名称默认为“main”。所以,在Application中应该为UI主线程添加UncaughtExceptionHandler,这样整个程序中的Activity、Service中出现的UncaughtException事件都可以被处理。
参考文档:
2. ANR 问题分析
2.1 ANR 类型
根据发生ANR的原因和超时时间分类,大致有四种ANR:
Provider:
Input:
Broadcast:
Service:
2.2 常见的ANR发生场景
1. 主线程频繁进行IO操作,比如读写文件或者数据库;
2. 硬件操作如进行调用照相机或者录音等操作;
3. 多线程操作的死锁,导致主线程等待超时;
4. 主线程操作调用join()方法、sleep()方法或者wait()方法;
5. 耗时动画/耗资源行为导致CPU负载过重
6. system server中发生WatchDog ANR;
7. service binder的数量达到上限
2.3 流程总结
前台服务,超时为SERVICE_TIMEOUT = 20s;
后台服务,超时为SERVICE_BACKGROUND_TIMEOUT = 200s
前台广播,超时为BROADCAST_FG_TIMEOUT = 10s;
后台广播,超时为BROADCAST_BG_TIMEOUT = 60s;
应用页面相应为5s;
Service, Broadcast, Input发生ANR之后,最终都会调用AMS.appNotResponding方法来处理超时,记录日志、弹出ANR的dialog等操作。
2.4 避免措施
在Activity和Service的生命周期方法(如onCreate()和onResume())里尽可能少的去做创建操作。建议使用Handler+Message的方式做一些耗时的创建操作。
避免在主线程上进行复杂耗时的操作。可以把耗时操作放在线程中去执行。
避免在BroadcastReceiver里做耗时的操作。如果要进行复杂的耗时操作,可以在onReceive()方法中启动一个Service来处理。
编写代码一定要仔细,避免出现同步/死锁或者错误处理不恰当等情况。
2.5 ANR 分析方法
1. Log
刚才产生ANR后,看下Log:
ANR Log.png
可以看到logcat清晰地记录了ANR发生的时间,以及线程的tid和一句话概括原因:`WaitingInMainSignalCatcherLoop`,大概意思为主线程等待异常。
最后一句`The application may be doing too much work on its main thread.`告知可能在主线程做了太多的工作。
2. traces.txt
刚才的log有第二句Wrote stack traces to '/data/anr/traces.txt'
,说明ANR异常已经输出到traces.txt
文件,使用adb命令把这个文件从手机里导出来:
1. cd到`adb.exe`所在的目录,也就是**Android SDK**的`platform-tools`目录,例如:
cd D:\Android