在移动应用开发及应用发布阶段经常碰到应用崩溃的情况。对于开发阶段出现的崩溃,开发者可以从后台日志中获取崩溃堆栈进行分析;而线上出现的崩溃,开发者看不到后台日志,难以获取崩溃堆栈。这就需要一款可以监控线上应用崩溃情况的工具,当应用出现崩溃时及时收集堆栈信息进行分析,然后上报给服务端,开发者就可以在控制台实时了解应用的崩溃情况。为了满足监控移动端线上崩溃的需求,我们打造了鹰眼监控系统。鹰眼支持iOS、Android系统及RN、Flutter开发框架,本文就主要介绍一下鹰眼在Android 平台上 Java 和 Native 层的崩溃监控实践。
捕获 Java 层的崩溃相对比较简单,系统为我们提供了专门的 Thread.UncaughtExceptionHandler 接口来处理:
/**
* Interface for handlers invoked when a <tt>Thread</tt> abruptly
* terminates due to an uncaught exception.
*
* @since 1.5
*/
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
*
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
UncaughtExceptionHandler 未捕获异常处理接口,当一个线程由于一个未捕获异常即将崩溃时,JVM 将会通过 getUncaughtExceptionHandler() 方法获取该线程的 UncaughtExceptionHandler,并将该线程和异常作为参数传给 uncaughtException()方法。如果没有显式设置线程的 UncaughtExceptionHandler,那么会将其 ThreadGroup 对象会作为 UncaughtExceptionHandler。如果其 ThreadGroup 对象没有特殊的处理异常的需求,那么就会调 getDefaultUncaughtExceptionHandler() 方法获取默认的 UncaughtExceptionHandler 来处理异常。
我们都知道应用程序通常都会创建很多线程,如果为每一个线程都设置一次 UncaughtExceptionHandler 未免太过麻烦,既然出现未处理异常后 JVM 最终都会调 getDefaultUncaughtExceptionHandler(),那么我们可以在应用启动时设置一个默认的未捕获异常处理器:
public class MyApp extends Application {
public void onCreate() {
super.onCreate();
MyCrashHandler handler = new MyCrashHandler();
Thread.setDefaultUncaughtExceptionHandler(handler);
}
}
Thread.setDefaultUncaughtExceptionHandler(handler) 方法如果被多次调用的话,会以最后一次传递的 handler 为准,所以如果用了第三方的统计模块,可能会出现失灵的情况。对于这种情况,在设置默认 hander 之前,可以先通过 getDefaultUncaughtExceptionHandler() 方法获取并保留旧的 hander,然后在默认 handler 的uncaughtException 方法中调用其他 handler 的 uncaughtException 方法,保证都会收到异常信息。
考虑到跨平台、高性能需求、安全加密、硬件交互、第三方库等原因,往往不可能全部使用纯 Java 语言开发,需要借助 Java 平台的 JNI 接口(Java Native Interface),使用 C/C++ 来实现部分功能。Android NDK 并没有针对 C/C++ 代码,也就是 Native 层产生的崩溃提供统一的处理接口,那么是不是就没办法处理了呢?
在Unix-like系统中,所有的崩溃都是编程错误或者硬件错误相关的,系统遇到不可恢复的错误时会触发崩溃机制让程序退出,如除零、段地址错误等。异常发生时,CPU通过异常中断的方式,触发异常处理流程,不同的处理器有不同的异常中断类型和中断处理方式。Linux把这些中断处理统一为信号量,可以注册信号量向量进行处理。既然 Android 系统也是基于 Linux 的,那么我们可以利用 Linux 的信号机制进行异常捕获。
信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号。进程之间可以互相通过系统调用kill发送软中断信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。如图是信号机制的大致流程:
信号的接收:接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态,此时进程暂时不知道有信号到来。
信号的检测:进程陷入内核态后,在返回用户态时会对收到的信号进行检测,如果是一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的处理函数。
信号的处理:执行信号处理函数的方法很巧妙,内核会在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时, 才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。
收到信号的进程对各种信号可以有不同的处理方法,处理方法可以分为三类:
第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理;
第二种是忽略某个信号,对该信号不做任何处理,就像未发生过一样;
第三种是对该信号的处理保留系统的默认值,这种缺省操作对大部分的信号是使进程终止。
结构体 sigaction 描述了信号的处理方式:
struct sigaction {
unsigned int sa_flags;
union {
sighandler_t sa_handler;
void (*sa_sigaction)(int, struct siginfo*, void*);
};
sigset_t sa_mask;
void (*sa_restorer)(void);
};
sa_handler 是一个参数为int,返回类型为void的函数指针,参数即为信号值,所以不能传递除信号值之外的任何信息,不能与sa_sigaction 同时设置;
sa_mask 指定在信号处理函数执行过程中,哪些信号应当被阻塞,默认当前信号本身被阻塞;
sa_flags 影响信号处理函数行为的标志位,比较重要的一个是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以传递到信号处理函数中;
sa_sigaction 指向信号处理函数的指针,函数的第一个参数为信号值,第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,第三个参数包含了调用堆栈、寄存器信息等一系列数据;
sa_restorer 已过时,POSIX不支持它,不应再使用。
siginfo_t {
int si_signo; // Signal number 信号量
int si_errno; // An errno value
int si_code; // Signal code 错误码
}
发生native崩溃之后,logcat中通常会打出如下信息:
signal 11 (SIGSEGV), code 0 (SI_USER), fault addr 0x0
根据code去查表,其实就可以知道发生native崩溃的大致原因:
定义信号处理函数:
static struct sigaction handler;
memset(&handler, 0, sizeof(handler));
sigemptyset(&handler.sa_mask);
handler.sa_flags = SA_SIGINFO | SA_ONSTACK;
handler.sa_sigaction = my_sigaction;
进程通过 sigaction() 函数来指定对某个信号的处理行为。
int sigaction(int sig, const struct sigaction* new_action, struct sigaction* old_action);
sig:代表信号量,可以是除 SIGKILL 和 SIGSTOP 外的任何一个特定有效的信号量,SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略。同一个信号在不同的系统中值可能不一样,所以建议最好使用为信号定义的名字。
new_action:指向结构体 sigaction 的一个实例的指针,该实例指定了对特定信号的处理,如果设置为空,进程会执行默认处理。
old_action:和参数 new_action 类似,只不过保存的是原来对相应信号的处理,也可设置为 NULL。
注册信号处理函数:
static struct sigaction old_sa[NSIG];
static const int SIGNALS[] = {SIGILL, SIGTRAP, SIGABRT, SIGBUS, SIGFPE, SIGSEGV, SIGPIPE};
int size = sizeof(SIGNALS) / sizeof(int);
for (int i = 0; i < size; i++) {
result = sigaction(SIGNALS[i], &handler, &old_sa[SIGNALS[i]]);
if (result != JNI_OK) {
...
}
}
兼容其他signal处理:某些信号在之前可能已经被注册了信号处理函数,我们需要保留旧的处理函数。
static void my_sigaction(int signal_code, siginfo_t *siginfo, void *context) {
...
//执行旧的信号处理函数
old_sa[signal_code].sa_sigaction(signal_code, siginfo, context);
}
SIGSEGV 很有可能是栈溢出引起的,系统会在同一个已经满了的栈上调用 SIGSEGV 的信号处理函数,又再一次引起异常信号。为了避免这种情况,可以使用 sigaltstack() 注册一个可选的栈,预先保留在紧急情况下使用的空间。系统会在危险情况下把栈指针指向新的栈,保证信号处理函数的运行。
stack_t stack;
memset(&stack, 0, sizeof(stack));
stack.ss_size = SIGSTKSZ;
stack.ss_sp = malloc(stack.ss_size);
Verify(stack.ss_sp == JNI_FALSE, "Could not allocate signal alternative stack", return);
stack.ss_flags = 0;
int result = sigaltstack(&stack, NULL);
Verify(result != JNI_OK, "Could not set signal stack", return);
Native 崩溃的堆栈可以从信号处理函数的第三个参数 context 中获取。
Android 4.1.1 以上、5.0 以下系统可以使用系统自带的 libcorkscrew.so 解析,5.0 以上系统中没有了 libcorkscrew.so,可以使用开源库 libunwind 或 libbacktrace,其实 libbacktrace 内部也是使用了 libunwind 进行解析,这里简单介绍一下 libbacktrace 的用法。
mFrameLines.clear();
std::unique_ptr<Backtrace> backtrace(Backtrace::Create(BACKTRACE_CURRENT_PROCESS, BACKTRACE_CURRENT_THREAD));
if (!backtrace->Unwind(0, context)) {
LOGW("Failed to unwind native stack");
}
for (size_t i = 0; i < backtrace->NumFrames(); i++) {
mFrameLines.push_back(String8(backtrace->FormatFrameData(i).c_str()));
}
可以看到,使用 libbacktrace 一共就三步:
使用 Backtrace::Create 创建一个 Backtrace 实例。
调用 Unwind 函数 unwind 一下 stack。
FormatFrameData 输出每个栈帧的文本信息(可以根据 frame 自己打印)。
鹰眼支持iOS、Android系统及RN、Flutter框架上的崩溃数据采集,对数据进行深度挖掘整理,提供了控制台界面,方便接入方查看崩溃数据统计信息。
1.支持崩溃数据的实时统计,可从影响用户数和崩溃次数两个维度查看。
2.支持按多种时间维度分析崩溃趋势。
3.支持崩溃 Top 排行,及时发现重点问题,Top排行可查看操作系统、设备、网络、渠道等多维度排行,及崩溃类型排行、占比等。
4.支持浏览特定版本的崩溃数据。
5.支持查看崩溃记录详细数据,包括崩溃堆栈、崩溃类型、设备和应用基本信息、页面记录及后台日志等信息。
本文主要介绍了 Android 平台 Java、Native 崩溃捕获的实现方案,该方案已经在鹰眼 Android 端使用,赋能了近百个产品,协助定位、解决了包括 Native 崩溃在内的诸多疑难问题。鹰眼系统一直在不断丰富功能,提升SDK性能及稳定性,目前为集团内移动应用监控崩溃数据、提升稳定性提供了技术保障。接下来我们计划将系统对外开放,为更多开发者提供支持,敬请期待。