鹰眼Android平台崩溃监控实践

在移动应用开发及应用发布阶段经常碰到应用崩溃的情况。对于开发阶段出现的崩溃,开发者可以从后台日志中获取崩溃堆栈进行分析;而线上出现的崩溃,开发者看不到后台日志,难以获取崩溃堆栈。这就需要一款可以监控线上应用崩溃情况的工具,当应用出现崩溃时及时收集堆栈信息进行分析,然后上报给服务端,开发者就可以在控制台实时了解应用的崩溃情况。为了满足监控移动端线上崩溃的需求,我们打造了鹰眼监控系统。鹰眼支持iOS、Android系统及RN、Flutter开发框架,本文就主要介绍一下鹰眼在Android 平台上 Java 和 Native 层的崩溃监控实践。

捕获Java崩溃

捕获 Java 层的崩溃相对比较简单,系统为我们提供了专门的 Thread.UncaughtExceptionHandler 接口来处理:

 /**

UncaughtExceptionHandler 未捕获异常处理接口,当一个线程由于一个未捕获异常即将崩溃时,JVM 将会通过 getUncaughtExceptionHandler() 方法获取该线程的 UncaughtExceptionHandler,并将该线程和异常作为参数传给 uncaughtException()方法。如果没有显式设置线程的 UncaughtExceptionHandler,那么会将其 ThreadGroup 对象会作为 UncaughtExceptionHandler。如果其 ThreadGroup 对象没有特殊的处理异常的需求,那么就会调 getDefaultUncaughtExceptionHandler() 方法获取默认的 UncaughtExceptionHandler 来处理异常。

我们都知道应用程序通常都会创建很多线程,如果为每一个线程都设置一次 UncaughtExceptionHandler 未免太过麻烦,既然出现未处理异常后 JVM 最终都会调 getDefaultUncaughtExceptionHandler(),那么我们可以在应用启动时设置一个默认的未捕获异常处理器:

public class MyApp extends Application {

Thread.setDefaultUncaughtExceptionHandler(handler) 方法如果被多次调用的话,会以最后一次传递的 handler 为准,所以如果用了第三方的统计模块,可能会出现失灵的情况。对于这种情况,在设置默认 hander 之前,可以先通过 getDefaultUncaughtExceptionHandler() 方法获取并保留旧的 hander,然后在默认 handler 的uncaughtException 方法中调用其他 handler 的 uncaughtException 方法,保证都会收到异常信息。

Linux 信号机制

考虑到跨平台、高性能需求、安全加密、硬件交互、第三方库等原因,往往不可能全部使用纯 Java 语言开发,需要借助 Java 平台的 JNI 接口(Java Native Interface),使用 C/C++ 来实现部分功能。Android NDK 并没有针对 C/C++ 代码,也就是 Native 层产生的崩溃提供统一的处理接口,那么是不是就没办法处理了呢?

在Unix-like系统中,所有的崩溃都是编程错误或者硬件错误相关的,系统遇到不可恢复的错误时会触发崩溃机制让程序退出,如除零、段地址错误等。异常发生时,CPU通过异常中断的方式,触发异常处理流程,不同的处理器有不同的异常中断类型和中断处理方式。Linux把这些中断处理统一为信号量,可以注册信号量向量进行处理。既然 Android 系统也是基于 Linux 的,那么我们可以利用 Linux 的信号机制进行异常捕获。

信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号。进程之间可以互相通过系统调用kill发送软中断信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。如图是信号机制的大致流程:

image

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

  • **信号的检测:**进程陷入内核态后,在返回用户态时会对收到的信号进行检测,如果是一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的处理函数。

  • **信号的处理:**执行信号处理函数的方法很巧妙,内核会在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时, 才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。

收到信号的进程对各种信号可以有不同的处理方法,处理方法可以分为三类:

  • 第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理;

  • 第二种是忽略某个信号,对该信号不做任何处理,就像未发生过一样;

  • 第三种是对该信号的处理保留系统的默认值,这种缺省操作对大部分的信号是使进程终止。

常见信号量类型:image.gif

捕获 Native 崩溃1定义信号处理函数

结构体 sigaction 描述了信号的处理方式:

struct sigaction {
  • sa_handler 是一个参数为int,返回类型为void的函数指针,参数即为信号值,所以不能传递除信号值之外的任何信息,不能与sa_sigaction 同时设置;

  • sa_mask 指定在信号处理函数执行过程中,哪些信号应当被阻塞,默认当前信号本身被阻塞;

  • sa_flags 影响信号处理函数行为的标志位,比较重要的一个是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以传递到信号处理函数中;

  • sa_sigaction 指向信号处理函数的指针,函数的第一个参数为信号值,第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,第三个参数包含了调用堆栈、寄存器信息等一系列数据;

  • sa_restorer 已过时,POSIX不支持它,不应再使用。

sa_sigaction 的第二个参数,结构体 siginfo_t 包含了信号携带的数据值:

siginfo_t {

发生native崩溃之后,logcat中通常会打出如下信息:

signal 11 (SIGSEGV), code 0 (SI_USER), fault addr 0x0

根据code去查表,其实就可以知道发生native崩溃的大致原因:

image

定义信号处理函数:

static struct sigaction handler;

2注册信号处理函数

进程通过 sigaction() 函数来指定对某个信号的处理行为。

#include <signal.h>
  • sig:代表信号量,可以是除 SIGKILL 和 SIGSTOP 外的任何一个特定有效的信号量,SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略。同一个信号在不同的系统中值可能不一样,所以建议最好使用为信号定义的名字。

  • new_action:指向结构体 sigaction 的一个实例的指针,该实例指定了对特定信号的处理,如果设置为空,进程会执行默认处理。

  • old_action:和参数 new_action 类似,只不过保存的是原来对相应信号的处理,也可设置为 NULL。

注册信号处理函数:

static struct sigaction old_sa[NSIG];

兼容其他signal处理:某些信号在之前可能已经被注册了信号处理函数,我们需要保留旧的处理函数。

static void my_sigaction(int signal_code, siginfo_t *siginfo, void *context) {

3设置异常处理栈

SIGSEGV 很有可能是栈溢出引起的,系统会在同一个已经满了的栈上调用 SIGSEGV 的信号处理函数,又再一次引起异常信号。为了避免这种情况,可以使用 sigaltstack() 注册一个可选的栈,预先保留在紧急情况下使用的空间。系统会在危险情况下把栈指针指向新的栈,保证信号处理函数的运行。

stack_t stack;

4解析堆栈

Native 崩溃的堆栈可以从信号处理函数的第三个参数 context 中获取。

Android 4.1.1 以上、5.0 以下系统可以使用系统自带的 libcorkscrew.so 解析,5.0 以上系统中没有了 libcorkscrew.so,可以使用开源库 libunwind 或 libbacktrace,其实 libbacktrace 内部也是使用了 libunwind 进行解析,这里简单介绍一下 libbacktrace 的用法。

mFrameLines.clear();

可以看到,使用 libbacktrace 一共就三步:

  • 使用 Backtrace::Create 创建一个 Backtrace 实例。

  • 调用 Unwind 函数 unwind 一下 stack。

  • FormatFrameData 输出每个栈帧的文本信息(可以根据 frame 自己打印)。

鹰眼介绍

鹰眼支持iOS、Android系统及RN、Flutter框架上的崩溃数据采集,对数据进行深度挖掘整理,提供了控制台界面,方便接入方查看崩溃数据统计信息。

1.支持崩溃数据的实时统计,可从影响用户数和崩溃次数两个维度查看。

image.gif

2.支持按多种时间维度分析崩溃趋势。

image.gif

3.支持崩溃 Top 排行,及时发现重点问题,Top排行可查看操作系统、设备、网络、渠道等多维度排行,及崩溃类型排行、占比等。

image.gif

image

4.支持浏览特定版本的崩溃数据。

image

5.支持查看崩溃记录详细数据,包括崩溃堆栈、崩溃类型、设备和应用基本信息、页面记录及后台日志等信息。

image

总结

本文主要介绍了 Android 平台 Java、Native 崩溃捕获的实现方案,该方案已经在鹰眼 Android 端使用,赋能了近百个产品,协助定位、解决了包括 Native 崩溃在内的诸多疑难问题。鹰眼系统一直在不断丰富功能,提升SDK性能及稳定性,目前为集团内移动应用监控崩溃数据、提升稳定性提供了技术保障。接下来我们计划将系统对外开放,为更多开发者提供支持,敬请期待。

服务推荐

发布了0 篇原创文章 · 获赞 0 · 访问量 364

猜你喜欢

转载自blog.csdn.net/weixin_46837673/article/details/105449245