注意:沿用同一指标
尽可能还原Crash现场:
1、根据堆栈及现场信息找答案
2、找共性:机型、OS、实验开关、资源包,考虑影响范围
3、线下复现、远程调试
出现未捕获异常,导致出现异常退出
Thread.setDefaultUncaughtExceptionHandler();
我们通过设置自定义的UncaughtExceptionHandler,就可以在崩溃发生的时候获取到现场信息。注意,这个钩子是针对单个进程而言的,在多进程的APP中,监控哪个进程,就需要在哪个进程中设置一遍ExceptionHandler。
获取主线程的堆栈信息:
Looper.getMainLooper().getThread().getStackTrace();
获取当前线程的堆栈信息:
Thread.currentThread().getStackTrace();
获取全部线程的堆栈信息:
Thread.getAllStackTraces();
第三方Crash监控工具如Fabric、腾讯Bugly,都是以字符串拼接的方式将数组StackTraceElement[]转换成字符串形式,进行保存、上报或者展示。
logcat日志流程是这样的,应用层 –> liblog.so –> logd,底层使用ring buffer来存储数据。获取的方式有以下三种:
native崩溃时,通过unwind只能拿到Native堆栈。我们希望可以拿到当时各个线程的Java堆栈。
优点:简单,兼容性好。
缺点:
借用Gityuan流程图如下所示:
1、首先发生crash所在进程,在创建之初便准备好了defaultUncaughtHandler,用来来处理Uncaught Exception,并输出当前crash基本信息;
2、调用当前进程中的AMP.handleApplicationCrash;经过binder ipc机制,传递到system_server进程;
3、接下来,进入system_server进程,调用binder服务端执行AMS.handleApplicationCrash;
4、从mProcessNames查找到目标进程的ProcessRecord对象;并将进程crash信息输出到目录/data/system/dropbox;
5、执行makeAppCrashingLocked:
6、再执行handleAppCrashLocked方法:
7、通过mUiHandler发送消息SHOW_ERROR_MSG,弹出crash对话框;
8、到此,system_server进程执行完成。回到crash进程开始执行杀掉当前进程的操作;
9、当crash进程被杀,通过binder死亡通知,告知system_server进程来执行appDiedLocked();
10、最后,执行清理应用相关的activity/service/ContentProvider/receiver组件信息。
流程图如下:
由于Crash进程中拥有一个Binder服务端ApplicationThread,而应用进程在创建过程调用attachApplicationLocked(),从而attach到system_server进程,在system_server进程内有一个ApplicationThreadProxy,这是相对应的Binder客户端。当Binder服务端ApplicationThread所在进程(即Crash进程)挂掉后,则Binder客户端能收到相应的死亡通知,从而进入binderDied流程。
特点:
上述都会产生相应的signal信号,导致程序异常退出
编译C/C++需将带符号信息的文件保留下来。
捕获到崩溃时,将收集到尽可能多的有用信息写入日志文件,然后选择合适的时机上传到服务器。
读取客户端上报的日志文件,寻找合适的符号文件,生成可读的C/C++调用栈。
核心:如何确保客户端在各种极端情况下依然可以生成崩溃日志。
提前申请文件句柄fd预留。
参考Breakpad重新封装Linux Syscall Support的做法以避免直接调用libc去分配堆内存。
Breakpad使用了fork子进程甚至孙进程的方式去收集崩溃现场,即便出现二次崩溃,也只是这部分信息丢失。
这里说下Breakpad缺点:
未来Chromium会使用Crashpad替代Breakpad。
改造Breakpad,增加Logcat信息,Java调用栈信息、其它有用信息。
一个Native Crash log信息如下:
堆栈信息中 pc 后面跟的内存地址,就是当前函数的栈地址,我们可以通过下面的命令行得出出错的代码行数
arm-linux-androideabi-addr2line -e 内存地址
下面列出全部的信号量以及所代表的含义:
#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 // 非法的系统调用
一般关注SIGILL, SIGABRT, SIGBUS, SIGFPE, SIGSEGV, SIGSTKFLT, SIGSYS即可。
要订阅异常发生的信号,最简单的做法就是直接用一个循环遍历所有要订阅的信号,对每个信号调用sigaction()。
注意:
首先,应收集崩溃现场的一些信息,如下:
/system/etc/event-log-tags
注意寻找共性问题
/proc/meminfo
当系统可用内存小于MemTotal的10%时,OOM、大量GC、系统频繁自杀拉起等问题非常容易出现
包括Java内存、RSS、PSS
PSS和RSS通过/proc/self/smap计算,可以得到apk、dex、so等更详细的分类统计。
大小:
/proc/self/status
具体分布情况:
/proc/self/maps
注意:
对于32位进程,32位CPU,虚拟内存达到3GB就可能会引起内存失败的问题。如果是64位的CPU,虚拟内存一般在3~4GB。如果支持64位进程,虚拟内存就不会成为问题。
如果应用堆内存和设备内存比较充足,但还出现内存分配失败,则可能跟资源泄漏有关。
限制数:
/proc/self/limits
一般单个进程允许打开的最大句柄个数为1024,如果超过800需将所有fd和文件名输出日志进行排查。
大小:
/proc/self/status
一个线程一般占2MB的虚拟内存,线程数超过400个比较危险,需要将所有tid和线程名输出到日志进行排查。
容易出现引用失效、引用爆表等崩溃。
通过DumpReferenceTables统计JNI的引用表,进一步分析是否出现JNI泄漏等问题。
在dalvik.system.VMDebug类中,是一个native方法,亦是static方法;在JNI中可以这么调用
jclass vm_class = env->FindClass("dalvik/system/VMDebug"); jmethodID dump_mid = env->GetStaticMethodID( vm_class, "dumpReferenceTables", "()V" ); env->CallStaticVoidMethod( vm_class, dump_mid );
接下来进行崩溃分析:
常见的崩溃类型有
SIGSEGV:空指针、非法指针等
SIGABRT:ANR、调用abort推出等
如果是ANR,先看主线程堆栈、是否因为锁等待导致,然后看ANR日志中的iowait、CPU、GC、systemserver等信息,确定是I/O问题或CPU竞争问题还是大量GC导致的ANR。
注意:
当从一条崩溃日志中无法看出问题原因时,需要查看相同崩溃点下的更多崩溃日志,或者也可以查看内存信息、资源信息等进行异常排查。
机型、系统、ROM、厂商、ABI这些信息都可以作为共性参考,对于下一步复现问题有明确指引。
复现之后再增加日志或使用Debugger、GDB进行调试。如不能复现,可以采用一些高级手段,如xlog日志、远程诊断、动态分析等等。
补充:系统崩溃解决方式
参考Android 8.0 try catch的做法,代理Toast里的mTN(handler)就可以实现捕获异常。
SP 调用 apply 方法,会创建一个等待锁放到 QueuedWork 中,并将真正数据持久化封装成一个任务放到异步队列中执行,任务执行结束会释放锁。Activity onStop 以及 Service 处理 onStop,onStartCommand 时,执行 QueuedWork.waitToFinish() 等待所有的等待锁释放。
所有此类 ANR 都是经由 QueuedWork.waitToFinish() 触发的,只要在调用此函数之前,将其中保存的队列手动清空即可。
具体是Hook ActivityThrad的Handler变量,拿到此变量后给其设置一个Callback,Handler 的 dispatchMessage 中会先处理 callback。最后在 Callback
中调用队列的清理工作,注意队列清理需要反射调用 QueuedWork。
注意:
apply 机制本身的失败率就比较高(1.8%左右),清理等待锁队列对持久化造成的影响不大。
它是由系统的FinalizerWatchdogDaemon抛出来的。
这里首先介绍下看门狗 WatchDog,它 的作用是监控重要服务的运行状态,当重要服务停止时,发生 Timeout 异常崩溃,WatchDog 负责将应用重启。而当关闭 WatchDog(执行stop()方法)后,当重要服务停止时,也不会发生 Timeout 异常,是一种通过非正常手段防止异常发生的方法。
stop方法,在Android 6.0之前会有线程同步问题。
因为6.0之前调用threadToStop的interrupt方法是没有加锁的,所以可能会有线程同步的问题。
注意:Stop的时候有一定概率导致即使没有超时也会报timeoutexception。
缺点:
只是为了避免上报异常采取的一种hack方案,并没有真正解决引起finialize超市的问题。
通过反射将输入法的两个View置空。
请参考 深入探索Android启动速度优化 一文。
这里补充一个方案,利用SyncAdapter提高进程优先级,它是Android系统提供一个账号同步机制,它属于核心进程级别,而使用了SyncAdapter的进程优先级本身也会提高,使用方式请Google,关联SyncAdapter后,进程的优先级变为1,仅低于前台正在运行的进程,因此可以降低应用被系统杀掉的概率。
缺点:高版本ROM需要root权限
解决方案:海外Google Play服务、国内Hardcoder
利用主线程的消息队列处理机制,应用发生卡顿,一定是在dispatchMessage中执行了耗时操作。我们通过给主线程的Looper设置一个Printer,打点统计dispatchMessage方法执行的时间,如果超出阀值,表示发生卡顿,则dump出各种信息,提供开发者分析性能瓶颈。
为卡顿监控代码增加ANR的线程监控,在发送消息时,在ANR线程中保存一个状态,主线程消息执行完后再Reset标志位。如果在ANR线程中收到发送消息后,超过一定时间没有复位,就可以任务发生了ANR。
缺点:
注意:由于traces.txt上传比较耗时,所以一般线下采用,线上建议综合ProcessErrorStateInfo和出现ANR时的堆栈信息来实现ANR的实时上传。
ANR发生原因:没有在规定的时间内完成要完成的事情。
从进程角度看发生原因有:
Andorid系统监测ANR的核心原理是消息调度和超时处理。
1、抓取bugreport
adb shell bugreport > bugreport.txt
2、直接导出/data/anr/traces.txt文件
adb pull /data/anr/traces.txt trace.txt
cpu负载Load: 7.58 / 6.21 / 4.83
代表此时一分钟有平均有7.58个进程在等待
1、5、15分钟内系统的平均负荷
当系统负荷持续大于1.0,必须将值降下来
当系统负荷达到5.0,表面系统有很严重的问题
cpu使用率
CPU usage from 18101ms to 0ms ago 28% 2085/system_server: 18% user + 10% kernel / faults: 8689 minor 24 major 11% 752/android.hardware.sensors@1.0-service: 4% user + 6.9% kernel / faults: 2 minor 9.8% 780/surfaceflinger: 6.2% user + 3.5% kernel / faults: 143 minor 4 major
上述表示Top进程的cpu占用情况。
注意:如果CPU使用量很少,说明主线程可能阻塞。
----- pid 10494 at 2019-11-18 15:28:29 -----
"main" prio=5 tid=1 Sleeping | group="main" sCount=1 dsCount=0 flags=1 obj=0x746bf7f0 self=0xe7c8f000 | sysTid=10494 nice=-4 cgrp=default sched=0/0 handle=0xeb6784a4 | state=S schedstat=( 5119636327 325064933 4204 ) utm=460 stm=51 core=4 HZ=100 | stack=0xff575000-0xff577000 stackSize=8MB | held mutexes=
关键字段含义:
1、sp调用apply导致anr问题?
虽然apply并不会阻塞主线程,但是会将等待时间转嫁到主线程。
2、检测运行期间是否发生过异常退出?
在应用启动时设定一个标志,在主动自杀或崩溃后更新标志 ,下次启动时检测此标志即可判断。
broadcast跟service超时机制大抵相同,但有一个非常隐蔽的技能点,那就是通过静态注册的广播超时会受SharedPreferences(简称SP)的影响。
当SP有未同步到磁盘的工作,则需等待其完成,才告知系统已完成该广播。并且只有XML静态注册的广播超时检测过程会考虑是否有SP尚未完成,动态广播并不受其影响。
对于Service, Broadcast, Input发生ANR之后,最终都会调用AMS.appNotResponding;
对于provider,在其进程启动时publish过程可能会出现ANR, 则会直接杀进程以及清理相应信息,而不会弹出ANR的对话框.
对于输入事件发生ANR,首先会调用InputMonitor.notifyANR,最终也会调用AMS.appNotResponding。
1、收集firstPids进程的stacks:
2、收集Native进程的stacks。(dumpNativeBacktraceToFile)
3、收集lastPids进程的stacks:
注意:
上述导出每个进程trace时,进程之间会休眠200ms。
灾包括:
传统流程:
用户反馈、重新打包、渠道更新、不可接受。
配置中心,服务端下发配置控制
针对场景:
微信读书、蘑菇街、淘宝、天猫等“重运营”的APP都使用了安全模式保障客户端启动流程,启动失败后给用户自救机会。先介绍一下它的核心特点:
配置后台:统一的配置后台,具备灰度发布机制
1、客户端能力:
2、数据统计及告警
3、快速测试
1、如何判断异常退出?
APP启动时记录一个flag值,满足以下条件时,将flag值清空
如果在启动阶段发生异常,则flag值不会清空,通过flag值就可以判断客户端是否异常退出,每次异常退出,flag值都+1。
2、安全模式的分级执行策略
分为两级安全模式,连续Crash 2次为一级安全模式,连续Crash 2次及以上为二级安全模式。
业务线可以在一级安全模式中注册行为,比如清空缓存数据,再进入该模式时,会使用注册行为尝试修复客户端
如果一级安全模式无法修复APP,则进入二级安全模式将APP恢复到初次安装状态,并将Document、Library、Cache三个根目录清空。
3、热修复执行策略
只要发现配置中需要热修复,APP就会同步阻塞进行热修复,保证修复的及时性
4、灰度方案
灰度时,配置中会包含灰度、正式两份配置及其灰度概率
APP根据特定算法算出自己是否满足灰度条件,则使用灰度配置
1、接入成本
完善文档、接口简洁
2、统一配置后台
可按照APP、版本配置
3、定制性
支持定制功能,让接入方来决定具体行为
4、灰度机制
5、数据分析
采用统一数据平台,为安全模式改进提供依据
6、快速测试
创建更多的针对性测试案例,如模拟连续Crash
功能开关 -> 统跳中心 -> 动态修复 -> 安全模式
根据以上三方面的优化我们搭建了移动端的高可用平台。
Android稳定性优化是一个需要长期投入,持续运营和维护的一个过程,上文中我们不仅深入探讨了Java Crash、Native Crash和ANR的解决流程及方案,还分析了其内部实现原理和监控流程。到这里,可以看到,要想做好稳定性优化,我们必须对虚拟机运行、Linux信号处理和内存分配有一定程度的了解,只有深入了解这些底层知识,我们才能比别人设计出更好的稳定性优化方案。