34

鹰眼Android平台崩溃监控实践

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzUyMDAxMjQ3Ng%3D%3D&%3Bmid=2247491512&%3Bidx=1&%3Bsn=fab12762f04570aaac7738e2437ad302
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

捕获Java崩溃

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

/**

* Interface for handlers invoked when a <tt>Thread</tt> abruptly

* terminates due to an uncaught exception.

*

* @since 1.5

*/

@FunctionalInterface

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 {

@Override

public void onCreate() {

super.onCreate();

MyCrashHandler handler = new MyCrashHandler();

Thread.setDefaultUncaughtExceptionHandler(handler);

}

}

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发送软中断信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。如图是信号机制的大致流程:

EBj6fub.jpg!web

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

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

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

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

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

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

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

常见信号量类型: 3q6B3mj.jpg!web

捕获 Native 崩溃

1

定义信号处理函数

结构体 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 不支持它,不应再使用。

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

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崩溃的大致原因:

FriYfuE.jpg!web

定义信号处理函数:

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;

2

注册信号处理函数

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

#include <signal.h>

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);

}

3

设置异常处理栈

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);

4

解析堆栈

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.支持崩溃数据的实时统计,可从影响用户数和崩溃次数两个维度查看。

ArE77zU.jpg!web

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

BbM7fyA.jpg!web

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

JnyM3ia.jpg!web

VnUV7rn.jpg!web

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

aIvQJzU.jpg!web

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

ue63ean.jpg!web

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK