Android Crash治理

现状

正式上线的程序上,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。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值