1.优化 - Android 异常捕获

  先来聊聊为什么要捕获奔溃。因为在开发或者测试阶段不能做到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把这些中断处理,统一为信号量,可以注册信号量向量进行处理。
  • 信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号。

函数运行在用户态,当遇到系统调用、中断或是异常的情况时,程序会进入内核态。信号涉及到了这两种状态之间的转换。


640.jpeg
  • 信号的接收
    接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。

  • 信号的检测,进程陷入内核态后,有两种场景会对信号进行检测:
    进程从内核态返回到用户态前进行信号检测
    进程在内核态中,从睡眠状态被唤醒的时候进行信号检测
    当发现有新信号时,便会进入下一步,信号的处理。

  • 信号的处理
    信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(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 层的崩溃捕获机制及实现

你可能感兴趣的:(1.优化 - Android 异常捕获)