先来聊聊为什么要捕获奔溃。因为在开发或者测试阶段不能做到100%的问题解决,因为 app 上线之后会有你想不到的各种各样的使用的场景,而发生问题时用户只能描述一下怎么怎么怎么就出现了问题。也许反馈到开发这边可以100%复现那就可以得到解决,但是也有可能在本地复现不了(PS:我没问题啊),只有在用户的手机上可以出现,这可能和用户使用的场景(温度太高导致CPU限速,温度太低等),手机的内存,CPU,老年机等等都有关系。
so,奔溃捕获的需求不就来了吗。
在 android 里面,奔溃可以分为二大类,一个为 java 奔溃,一个为 native 奔溃。对于这二种奔溃需要用不同的方式去捕获。
1.java奔溃
对于发生在 java 层的奔溃是比较好处理的,在主线程里面设置
//Thread 原来的捕获处理机制
Thread.UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
e.printStackTrace();
//1.发生奔溃时,清空 app 状态信息,把 activity 和 service 全部 finish
//2.状态记录上报(手机的现场信息尽量拿到,手机基本信息,堆栈,内存值,cpu运行值等等,有操作轨迹是最好的了)
//3.根据自己业务做处理
// 3.1 可以退到首页,不闪退到桌面
// 3.2 走系统默认逻辑,即 defaultHandler.uncaughtException(t,e);
}
});
- 为什么要清空 app 状态信息,把 activity 和 service 全部 finish
p : 状态清空-有可能在内存中写入了坏的数据
p : 奔溃后手机的默认处理(百度得)
1.java奔溃,activity 栈只有一个是闪退到桌面
2.java奔溃,activity 栈有多个时,将 app 杀死后,系统检测到异常退出会把将 app 拉起到上一个页面,并走其生命周期,对于上一个页面之前的页面不走生命周期,并且会重启所有的服务
3.native奔溃,activity 栈只有一个是闪退到桌面
4.native奔溃,activity 栈有多个时,将 app 杀死后,系统检测到异常退出会把将 app 拉起到上一个页面,并走其生命周期,对于上一个页面之前的页面不走生命周期,不会重启所有的服务
2.native奔溃捕获
在平时,可能写 native 的代码都比较少,跟别说去捕获 native 的异常了。那 Native 崩溃又是怎么产生的呢?一般都是因为在 Native 代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动 abort,这些都会产生相应的 signal 信号,导致程序异常退出。
我们可以查一些资料或者第三方的开源库,只需要从以下几个方面入手即可:
- 了解 native 层的崩溃处理机制
- 捕捉到 native crash 信号
- 处理各种特殊情况
- 解析 native 层的 crash 堆栈
2.1了解 native 层的崩溃处理机制
- 在Unix-like系统中,所有的崩溃都是编程错误或者硬件错误相关的,系统遇到不可恢复的错误时会触发崩溃机制让程序退出,如除零、段地址错误等。
- 异常发生时,CPU通过异常中断的方式,触发异常处理流程。不同的处理器,有不同的异常中断类型和中断处理方式。
- linux把这些中断处理,统一为信号量,可以注册信号量向量进行处理。
- 信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号。
函数运行在用户态,当遇到系统调用、中断或是异常的情况时,程序会进入内核态。信号涉及到了这两种状态之间的转换。
信号的接收
接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。信号的检测,进程陷入内核态后,有两种场景会对信号进行检测:
进程从内核态返回到用户态前进行信号检测
进程在内核态中,从睡眠状态被唤醒的时候进行信号检测
当发现有新信号时,便会进入下一步,信号的处理。信号的处理
信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数。接下来进程返回到用户态中,执行相应的信号处理函数。
信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理。如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。
至此,一个完整的信号处理流程便结束了,如果同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行。
2.2捕捉到 native crash 信号
在了解完原理之后,我们在 posix 可以使用信号处理函数捕获到 native crash(SIGSEGV, SIGBUS等) 来捕获异常。
#include
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
signum:代表信号编码,可以是除SIGKILL及SIGSTOP外的任何一个特定有效的信号,如果为这两个信号定义自己的处理函数,将导致信号安装错误。
act:指向结构体sigaction的一个实例的指针,该实例指定了对特定信号的处理,如果设置为空,进程会执行默认处理。
oldact:和参数act类似,只不过保存的是原来对相应信号的处理,也可设置为NULL。
这样之后,我们就可以捕获到 native 的异常了。
2.3处理各种特殊情况
- 文件句柄泄漏,导致创建日志文件失败,怎么办?
应对方式:我们需要提前申请文件句柄 fd 预留,防止出现这种情况。 - 因为栈溢出了,导致日志生成失败,怎么办?
应对方式:为了防止栈溢出导致进程没有空间创建调用栈执行处理函数,我们通常会使用常见的 signalstack。在一些特殊情况,我们可能还需要直接替换当前栈,所以这里也需要在堆中预留部分空间。 - 整个堆的内存都耗尽了,导致日志生成失败,怎么办?
应对方式:这个时候我们无法安全地分配内存,也不敢使用 stl 或者 libc 的函数,因为它们内部实现会分配堆内存。这个时候如果继续分配内存,会导致出现堆破坏或者二次崩溃的情况。 - 堆破坏或二次崩溃导致日志生成失败,怎么办?
应对方式:从原进程 fork 出子进程去收集崩溃现场,此外涉及与 Java 相关的,一般也会用子进程去操作。这样即使出现二次崩溃,只是这部分的信息丢失,我们的父进程后面还可以继续获取其他的信息。在一些特殊的情况,我们还可能需要从子进程 fork 出孙进程。
2.4解析 native 层的 crash 堆栈
在检测到有异常之后,可以利用一些系统机制拿到 pid ,tid ,符号表解析可以拿到 native 堆栈,根据相关异常信号可以解析出异常原因,之后在去拿 java 的堆栈等
代码实现参考 Android 平台 Native 代码的崩溃捕获机制及实现
- 1.注册信号
// 异常信号量
const int exceptionSignals[] = {SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS, SIGTRAP};
const int exceptionSignalsNumber = sizeof(exceptionSignals) / sizeof(exceptionSignals[0]);
void signalAction(int code, struct siginfo *si, void *sc) {
LOGD("检测到了native crash");
//防止出现死循环
signal(code, SIG_DFL);
signal(SIGALRM, SIG_DFL);
(void) alarm(0);
//开始收集处理
notifySignal(code, si, sc);
//调用久的处理
oldHandlers[code].sa_sigaction(code, si, sc);
}
bool setSignalHandle() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
//指定信号处理函数
sa.sa_sigaction = signalAction;
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
for (int i = 0; i < exceptionSignalsNumber; ++i) {
sigaddset(&sa.sa_mask, exceptionSignals[i]);
}
for (int i = 0; i < exceptionSignalsNumber; ++i) {
if (sigaction(exceptionSignals[i], &sa, &oldHandlers[exceptionSignals[i]]) == -1) {
LOGE("set sigaction error");
return false;
}
}
return true;
}
- 2设置额外的栈空间
int sigaltstack(const stack_t *ss, stack_t *oss);
//1.SIGSEGV很有可能是栈溢出引起的,如果在默认的栈上运行很有可能会破坏程序运行的现场,无法获取到正确的上下文。而且当栈满了(太多次递归,
// 栈上太多对象),系统会在同一个已经满了的栈上调用SIGSEGV的信号处理函数,又再一次引起同样的信号。
//2.我们应该开辟一块新的空间作为运行信号处理函数的栈。可以使用sigaltstack在任意线程注册一个可选的栈,保留一下在紧急情况下使用的空间。(系统会在
// 危险情况下把栈指针指向这个地方,使得可以在一个新的栈上运行信号处理函数)
void setStackSize() {
stack_t new_stack;
stack_t old_stack;
memset(&new_stack, 0, sizeof(new_stack));
memset(&old_stack, 0, sizeof(old_stack));
static const unsigned sigaltstackSize = std::max(16384, SIGSTKSZ);
if (sigaltstack(NULL, &old_stack) == -1 || !old_stack.ss_sp || old_stack.ss_size < sigaltstackSize) {
new_stack.ss_sp = calloc(1, sigaltstackSize);
new_stack.ss_size = sigaltstackSize;
if (sigaltstack(&new_stack, NULL) == -1) {
free(new_stack.ss_sp);
LOGE("sigaltstack error");
}
}
}
- 3.拿到相关信息
//而在 notifySignal(code, si, sc) 函数中主要做了下面几件事
//pid
getpid();
//tid
gettid();
//processName
//主要是打开 “/proc/pid/cmdline" 这个文件来拿到进程名
//threadName(主线程为 main)
//主要是打开 “/proc/tid/comm" 这个文件来拿到线程名(子线程)
//java 堆栈 通过 jni 掉用到 java 层 子线程需要过滤下
Thread.getStackTrace();
//native 堆栈
//原理:通过栈基址找上一个栈基址,可以得到函数调用链。通过栈基址偏移量可以的到 pc 值,之后用 dladdr 或者 add2line 可以拿到函数名字
_Unwind_Reason_Code unwind_callback(struct _Unwind_Context *unwindContext, void *arg) {
const uintptr_t pc = _Unwind_GetIP(unwindContext);
if (pc != 0x0) {
LOGD("pc = %p", pc);// pc 值
}
}
_Unwind_Backtrace(unwind_callback, handlerContext);
// pcs 上面收集的 pc 个数 , crashContext->frames[i] pc 值数组
for (int i = 0; i < pcs; ++i) {
uintptr_t pc = crashContext->frames[i];
Dl_info info;
const void *addr = (void *) (pc);
if (dladdr(addr, &info) != 0 && info.dli_fname != NULL) {
const uintptr_t near = (uintptr_t) info.dli_saddr;
const uintptr_t offset = pc - near;
const uintptr_t addr_real = pc - (uintptr_t) info.dli_fbase;
const uintptr_t addr_to_use = is_dll(info.dli_fname) ? addr_real : pc;
LOGE("native crash #%02lx pc 0x%16lx %s (%s + 0x%lx)", i, addr_to_use, info.dli_fname, info.dli_sname, offset);
}
}
最后可以得到
javaInfo :
com.dabaicai.moniter.nativecrash.NativeCrashActivity.triggerNativeCrash(Native Method)
com.dabaicai.moniter.nativecrash.NativeCrashActivity.onClick(NativeCrashActivity.java:28)
android.view.View.performClick(View.java:7259)
android.view.View.performClickInternal(View.java:7236)
android.view.View.access$3600(View.java:801)
android.view.View$PerformClick.run(View.java:27892)
android.os.Handler.handleCallback(Handler.java:883)
android.os.Handler.dispatchMessage(Handler.java:100)
android.os.Looper.loop(Looper.java:214)
android.app.ActivityThread.main(ActivityThread.java:7356)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
nativeInfo :
native crash #00 pc ef80 /data/app/com.dabaicai.moniter-zZ2tp7JwMuZUOud06e4FNQ==/base.apk!/lib/arm64-v8a/libnative-crash.so (_Z16copyInfo2ContextiP7siginfoPv + 0xec)
native crash #01 pc ee54 /data/app/com.dabaicai.moniter-zZ2tp7JwMuZUOud06e4FNQ==/base.apk!/lib/arm64-v8a/libnative-crash.so (_Z12notifySignaliP7siginfoPv + 0x60)
native crash #02 pc e4d8 /data/app/com.dabaicai.moniter-zZ2tp7JwMuZUOud06e4FNQ==/base.apk!/lib/arm64-v8a/libnative-crash.so (_Z12signalActioniP7siginfoPv + 0xa0)
native crash #03 pc 5d6d181cec /system/bin/app_process64 ((null) + 0x5d6d181cec)
native crash #04 pc 7620cb5690 [vdso] (__kernel_rt_sigreturn + 0x0)
native crash #05 pc 684 /data/app/com.dabaicai.moniter-zZ2tp7JwMuZUOud06e4FNQ==/base.apk!/lib/arm64-v8a/libtest.so (Java_com_dabaicai_moniter_nativecrash_NativeCrashActivity_triggerNativeCrash + 0x1c)
native crash #06 pc 13f354 /apex/com.android.runtime/lib64/libart.so ((null) + 0x759a543354)
native crash #07 pc 136338 /apex/com.android.runtime/lib64/libart.so ((null) + 0x759a53a338)
native crash #08 pc 1450b0 /apex/com.android.runtime/lib64/libart.so (_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc + 0xf8)
native crash #09 pc 2e269c /apex/com.android.runtime/lib64/libart.so (_ZN3art11interpreter34ArtInterpreterToCompiledCodeBridgeEPNS_6ThreadEPNS_9ArtMethodEPNS_11ShadowFrameEtPNS_6JValueE + 0x184)
native crash #0a pc 2dd728 /apex/com.android.runtime/lib64/libart.so (_ZN3art11interpreter6DoCallILb0ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE + 0x388)
native crash #0b pc 5a2c9c /apex/com.android.runtime/lib64/libart.so (MterpInvokeDirect + 0x194)
native crash #0c pc 130918 /apex/com.android.runtime/lib64/libart.so ((null) + 0x759a534918)
native crash #0d pc 5a2400 /apex/com.android.runtime/lib64/libart.so (MterpInvokeInterface + 0x6d0)
native crash #0e pc 130a18 /apex/com.android.runtime/lib64/libart.so ((null) + 0x759a534a18)
native crash #0f pc 5a0c14 /apex/com.android.runtime/lib64/libart.so (MterpInvokeVirtual + 0x59c)
native crash #10 pc 130818 /apex/com.android.runtime/lib64/libart.so ((null) + 0x759a534818)
native crash #11 pc 5a2f9c /apex/com.android.runtime/lib64/libart.so (MterpInvokeDirect + 0x494)
native crash #12 pc 130918 /apex/com.android.runtime/lib64/libart.so ((null) + 0x759a534918)
native crash #13 pc 5a37a4 /apex/com.android.runtime/lib64/libart.so (MterpInvokeStatic + 0x474)
native crash #14 pc 130998 /apex/com.android.runtime/lib64/libart.so ((null) + 0x759a534998)
native crash #15 pc 5a2400 /apex/com.android.runtime/lib64/libart.so (MterpInvokeInterface + 0x6d0)
native crash #16 pc 130a18 /apex/com.android.runtime/lib64/libart.so ((null) + 0x759a534a18)
native crash #17 pc 5a37a4 /apex/com.android.runtime/lib64/libart.so (MterpInvokeStatic
文章参考:
- Android 平台 Native 代码的崩溃捕获机制及实现
- Android 开发高手 - 01 | 崩溃优化(上):关于“崩溃”那些事儿
- 异常处理 - Native 层的崩溃捕获机制及实现