现状
正式上线的程序上,Crash是最破坏用户体感的一种行为。Crash率因此也成为衡量一个App稳定性最重要的标志。
Android系统可定制化而导致的碎片化、业务的高速发展、多商家定制业务、多人员协同开发等,导致的crash率无法稳定保持在安全线下。
Crash治理总体方案
针对线上与测试中遇到的各种Crash,我们的处理方式通常是发现一个,处理一个。而进一步,则是希望在处理过程中能将某些有共性的Crash的处理方式提取出来,并在后续能做到有效预防。
总体来说,治理方案应该是:
1、从单一处理,逐渐提升到同一种问题的通用处理,由点及面;
2、以在处理Crash过程中得到的经验,对后续可能出现Crash的地方,做有效的预防处理。Crash是病,预防胜于已经生病了再治理;
3、当Crash真正发生时,要尽可能快的止损。改动很小或者是方便降级,目前我们的方案可以是热修复和强制升级。
Crash多种多样,我们针对不同的Crash,有不同的处理经验。
Crash分类
分为:与Java源码相关的Java Crash 、与系统(C、C++相关)的Native Crash
一、Java Crash
常见异常:
1、空指针(NullPointException)
这大概是发生频率最高的一种Crash,可能由各种原因产生,最终表现为空指针。
(1)服务端接口返回脏数据或者空,导致原本测试正常的程序出现Crash;
(2)可能为空的字段,未加判断导致;
(3)全局静态变量,因为内存原因被回收导致引用该变量地方出现Crash;
解决方法:以上情况,从Crash的堆栈中可以较为容易的看出并解决。如是为空等情况,做好程序判空,对于需要用到全局静态变量的地方,做好本地持久化。
2、角标越界(IndexOutOfBoundsException)
数组、集合在使用过程中可能出现的一种情况。
常见使用错误例子如下:
(1)使用subString,入参使用的长度超过字符串整体长度;
(2)遍历数组、集合数据的同时,对数组、集合做删除操作;
解决方法:使用数组、集合前,对长度等做判断,在遍历时,不做删除等操作。
3、类型转换异常(ClassCastException)
强转导致的异常,如String str=(Strign)x;
解决方法:这种错误在编译过程中基本能发现。建议使用安全类型转换函数,并设置默认值,避免转换后值不符合要求导致的Crash。
4、类找不到(ClassNotFoundException)
出现该问题的主要可能是混淆导致。在本地debug正常,在线上release出现。
解决方法:增加混淆规则,对不想混淆的类做处理。
5、窗体句柄泄漏(Activity has leaked...)
基本出现在Dialog等还在,Activity已finish的情况。服务端数据异步返回,较容易出现该问题。
解决方法:在Activity finish时,用dismiss或者置为null等方法,解除窗体引用。
6、DeadObjectException
该错误产生的原因是某个对象已经被回收,但是系统中还是在使用它导致的。
在多进程中(例如AIDL)中,当一个进程被Kill后,另一个与之相关的进程有可能产生该错误提示;
解决方法:对多进程做统一的管理,可在主进程被Kill的时候,关闭所有子进程。二者保持一致性。
7、Package manager has died
该错误发生,说明该进程已经不存在。由于某些错误原因PackageManager进程已经退出,此时任何向它请求的都会出错。
该问题主要发生在多进程中,当某一个进程崩溃,导致进程重启等导致;
解决方法:对多进程做统一的管理,可在主进程被Kill的时候,关闭所有子进程。二者保持一致性。
8、Cursor未关闭(android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed)
这个异常是因为使用SQLLite时忘记释放游标导致,内存泄漏,最后出现崩溃。
解决方法:在必要的地方,手动关闭游标。
9、文件句柄太多没有被关闭(Could not read input channel file descriptors from parcel)
本地I/O操作过多,文件句柄没有被关闭导致。
解决方法:检查I/O操作中,频繁操作文件处,对这些操作做优化处理。
以上是目前出现频次较多的异常,部分已经解决,部分还待优化代码。具体解决方案将在后续的 预防Crash中列出。
10、内存溢出-OOM异常
该异常通常是因为系统未能及时释放已经不再使用的内存对象,导致最终溢出而引发的崩溃。出现该类异常时,异常堆栈并不能作为一个很好的参考。
需要对程序运行流程中可能涉及到的内存泄漏地方做处理,避免泄漏。
在Android平台上,最常见也是最严重的内存泄漏就是Activity对象泄漏。Activity承载了App的整个界面功能,Activity的泄漏同时也意味着它持有的大量资源对象都无法被回收,极其容易造成OOM。
常见的可能会造成Activity泄漏的原因有:
(1)匿名内部类实现Handler处理消息,可能导致隐式持有的Activity对象无法回收。
(2)Activity和Context对象被混淆和滥用,在许多只需要Application Context而不需要使用Activity对象的地方使用了Activity对象,比如注册各类Receiver、计算屏幕密度等等。
(3)View对象处理不当,使用Activity的LayoutInflater创建的View自身持有的Context对象其实就是Activity,这点经常被忽略,在自己实现View重用等场景下也会导致Activity泄漏。
解决方案:
(1)对于Activity泄漏,目前已经有了一个非常好用的检测工具:LeakCanary,它可以自动检测到所有Activity的泄漏情况,并且在发生泄漏时给出十分友好的界面提示,同时为了防止开发人员的疏漏,我们也会将其上报到服务器,统一检查解决。具体可以在代码依赖中只在debug时,依赖LeakCanary;
(2)使用Android studio自带的profile,检查运行过程中可能出现的泄漏情况,并修复。
11、ANR异常
在主线程中进行耗时操作,超过5s会导致该问题。
解决方案:出现ANR异常时,可针对出现该问题的场景,做多次重试,定位耗时长位置并修复。
二、Native Crash
即C/C++层面的Crash,这是介于系统framework层与Linux层之间的一层。在Android中主要表现为so文件crash。
Crash的信息展示如下:
可根据signal来区分大致crash原因:
#define SIGHUP 1 // 终端连接结束时发出(不管正常或非正常)
#define SIGINT 2 // 程序终止(例如Ctrl-C)
#define SIGQUIT 3 // 程序退出(Ctrl-\)
#define SIGILL 4 // 执行了非法指令,或者试图执行数据段,堆栈溢出
#define SIGTRAP 5 // 断点时产生,由debugger使用
#define SIGABRT 6 // 调用abort函数生成的信号,表示程序异常
#define SIGIOT 6 // 同上,更全,IO异常也会发出
#define SIGBUS 7 // 非法地址,包括内存地址对齐出错,比如访问一个4字节的整数, 但其地址不是4的倍数
#define SIGFPE 8 // 计算错误,比如除0、溢出
#define SIGKILL 9 // 强制结束程序,具有最高优先级,本信号不能被阻塞、处理和忽略
#define SIGUSR1 10 // 未使用,保留
#define SIGSEGV 11 // 非法内存操作,与SIGBUS不同,他是对合法地址的非法访问,比如访问没有读权限的内存,向没有写权限的地址写数据
#define SIGUSR2 12 // 未使用,保留
#define SIGPIPE 13 // 管道破裂,通常在进程间通信产生
#define SIGALRM 14 // 定时信号,
#define SIGTERM 15 // 结束程序,类似温和的SIGKILL,可被阻塞和处理。通常程序如果终止不了,才会尝试SIGKILL
#define SIGSTKFLT 16 // 协处理器堆栈错误
#define SIGCHLD 17 // 子进程结束时, 父进程会收到这个信号。
#define SIGCONT 18 // 让一个停止的进程继续执行
#define SIGSTOP 19 // 停止进程,本信号不能被阻塞,处理或忽略
#define SIGTSTP 20 // 停止进程,但该信号可以被处理和忽略
#define SIGTTIN 21 // 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号
#define SIGTTOU 22 // 类似于SIGTTIN, 但在写终端时收到
#define SIGURG 23 // 有紧急数据或out-of-band数据到达socket时产生
#define SIGXCPU 24 // 超过CPU时间资源限制时发出
#define SIGXFSZ 25 // 当进程企图扩大文件以至于超过文件大小资源限制
#define SIGVTALRM 26 // 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
#define SIGPROF 27 // 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间
#define SIGWINCH 28 // 窗口大小改变时发出
#define SIGIO 29 // 文件描述符准备就绪, 可以开始进行输入/输出操作
#define SIGPOLL SIGIO // 同上,别称
#define SIGPWR 30 // 电源异常
#define SIGSYS 31 // 非法的系统调用
以图片中Crash信息为例,能找到对应信息:非法内存操作,与SIGBUS不同,他是对合法地址的非法访问,比如访问没有读权限的内存,向没有写权限的地址写数据。
更详细的错误信息,与Java Crash类似,可以查看堆栈信息。在上图里,堆栈信息中 pc 后面跟的内存地址,就是当前函数的栈地址,我们可以通过命令行arm-linux-androideabi-addr2line -e 内存地址得出出错的代码行数了。
三、Crash 预防
1、内存泄漏的检查。这是造成OOM最多的原因,debug模式下嵌入LeakCanary,注意页面销毁时的资源处理等;
2、主进程读写操作等不过于频繁,避免引起ANR;
3、工程架构合理优化。分层明确,有框架可依,避免太多样的写法,导致不可控;可以参考Lifecycle与ViewModel结合模式,可以尽可能的避免因为生命周期等引发的窗体泄漏等;
4、可能引发NPE等地方,入口多做判断,结合anotation(@NOTNULL)等使用;
5、严格代码规范,可根据公司代码规范提示,或者新增Link强制规范;
6、作为一个多进程程序,在主进程结束或者意外结束时,需要强制停止其他子进程。避免二次Crash。