本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
在程序发生 ANR 时,系统会弹出 ANR 的弹窗,并将 ANR 日志信息写入到 /data/anr/ 目录下的文件中,但是我们并没有直接的接口去感知到 ANR 发生了,也没有权限去读取 /data/anr/ 目录下的文件。但是为了提升程序的稳定性,对线上的 ANR 进行有效的监控是必不可少的,因此就需要我们在程序去中实现一套 ANR 的监控的方案。
1 信号捕获检测方案
ANR 发生时,系统会给对应的进程发送一个 SIGQUIT 信号,既然是信号,那么我们就可以通过来捕获该 SIGQUIT 信号来实现对 ANR 的捕获。使用 sigaction 函数,增加 SIGQUIT 信号的捕获即可,并且在信号处理函数函数中判断 SIGQUIT 并做相应的处理,代码如下:
sigaction(SIGQUIT, &sa, &old_sa[SIGILL]);
但是在实际运行这段代码后,会发现我们自定义的异常处理函数并没有捕获到 SIGQUIT 信号,那是因为系统屏蔽了 SIGQUIT 信号,不允许 sigaction 接收 SIGQUIT 信号 。
当程序启动时会启动一个 SignalCatcher 线程,该线程会通过 sigwait 函数阻塞监听 SIGQUIT 信号,源码实现如下图,它位于 signal_catcher.cc 文件中。sigwait 函数也是监听信号的函数,相比于 sigaction 函数,它是同步接收的方式,也就是只允许有一个地方监听指定的信号,而 sigaction 函数则是异步的,可以在多个地方都监听执行信号并进行处理。
虽然系统屏蔽了 sigaction 异步接收 SIGQUIT 信号的方式,但是没法屏蔽通过 sigwait 同步监听 SIGQUIT 信号 ,这样也保障了 SignalCatcher 线程接收到 SIGQUIT 信号后,能够正常的获取进程中的各个线程的信息,并输出到 /data/anr/traces.txt 文件中。
虽然 SIGQUIT 信号被系统屏蔽了,但是我们可以使用 pthread_sigmask 函数将 SIGQUIT 信号从当前线程的信号屏蔽集中移除,实现如下
// 定义一个信号集
sigset_t new_set, old_set ;
// 初始化清空信号集
sigemptyset(&new_set);
// 将 SIGQUIT 信号添加到信号集
sigaddset(&new_set, SIGQUIT);
// 将当前线程的信号屏蔽集设置为信号集的补集,即解除 SIGQUIT 信号的屏蔽
pthread_sigmask(SIG_UNBLOCK, &new_set, &old_set);
当解除对 SIGQUIT 信号的屏蔽后,上面对 SIGQUIT 信号的捕获就能生效了,我们可以在信号的处理函数中来实现对 ANR 的处理,捕获 ANR 的 Trace数据,并且还要保证原来的 SignalCatcher 线程是能响应 SIGQUIT 信号的,因为 SignalCatcher 线程不是通过 sigaction 来响应 SIGQUIT 信号的,所以我们直接执行 old_sa 是没法生效的,此时可以通过 tgkill 信号发生函数 tgkill,往 SignalCatcher 线程发生一个 SIGQUIT 信号,tgkill 函数往指定线程发送信号时需要知道线程的 id,所以我们还需要通过遍历 /proc/{pid}/task 目录下所记录的该进程下所有的线程数据,拿到名称为 “SignalCatcher” 线程对应的线程 id。代码实现如下。
// 信号处理函数
static void signal_handler(int sig, siginfo_t *info, void *secret) {
if (sig == SIGQUIT) {
//捕获ANR信息
dealAnr();
//往 SignalCatcher 线程发送 SIGQUIT 信号
tgkill(getpid(), getSignalCatcherThreadId(), SIGQUIT);
}
}
//遍历/proc/[pid]/task目录,找到SignalCatcher线程的tid
int getSignalCatcherThreadId() {
// 构造 /proc/[pid] 目录的路径
string proc_path = "/proc/" + getpid();
DIR* dir = opendir(proc_path.c_str());
while ((entry = readdir(dir)) != NULL) {
std::string name = entry->d_name;
// 检查名称是否是一个数字
if (std::all_of(name.begin(), name.end(), ::isdigit)) {
// 构造 /proc/[pid]/task/[tid]/status 文件的路径
std::string status_path = proc_path + "/task/" + name + "/status";
std::ifstream status_file(status_path);
// 读取文件内容
std::string line;
while (std::getline(status_file, line)) {
// 查找 Name: SignalCatcher 这一行
if (line == "Name:\tSignalCatcher") {
// 找到了,返回该 tid
int tid = std::stoi(name);
closedir(dir);
return tid;
}
}
}
}
closedir(dir);
return -1;
}
上面的流程中我们便实现了通过监听 SIGQUIT 信号来监控是否发生了 ANR 了,但是实际情况中,当进程收到了 SIGQUIT 信号,只能说明当前进程有可能发生了 ANR ,但是并不能够百分百确定发生了 ANR,比如其他应用发生 ANR 时, CPU 使用率占用比较高的进程也会收到 SIGQUIT 信号,其他进程或者线程也可以手动通过 kill 或者 tgkill 等函数发 SIGQUIT 信号给当前进程,因此收到 SIGQUIT 信号只是进程发生了 ANR 的必要不充分条件。在实际场景中,我们会通过再补充一些方案来进行二次判断增加 ANR 判断成功率,所以笔者接着介绍补充方案。
2 AMS 接口检测方案
当前面信号捕获的逻辑中捕获到 SIGQUIT 信号后,可以通过 JNI 调用 Java 层的方法,通知 Java 层来对 ANR 进行二次确认。代码实现如下,在代码中,通过 CallStaticVoidMethod 函数来执行 Java 层的 onANRDumpTrace 函数。
void anrDumpTraceCallback(JNIEnv *env) {
// 找到对应的类和方法签名
jclass myUtilsClass = env->FindClass("com/example/app/MyUtils");
jmethodID onANRDumpTraceMethod =
env->GetStaticMethodID(myUtilsClass, "onANRDumpTrace", "()V");
// 调用方法
env->CallStaticVoidMethod(myUtilsClass, onANRDumpTraceMethod);
}
在 ActivityManagerService 通知进程启动 ANR 弹窗前,会给发生了 ANR 的进程设置一个 “NOT_RESPONDING” 的标志位,表示该进程发生了异常,而这个标识位是可以通过 ActivityManager 的 getProcessesInErrorState 方法来获取。因此在 onANRDumpTrace 函数中,我们可以调用该方法获取进程的错误状态,如果进程是 NOT_RESPONDING 状态,则说明进程发生了 ANR,此时便完成了对 ANR 的二次确认,代码实现如下。
void onANRDumpTrace() {
ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
if (am != null) {
List<ActivityManager.ProcessErrorStateInfo> errorList
= am.getProcessesInErrorState();
if (errorList != null && !errorList.isEmpty()) {
for (ActivityManager.ProcessErrorStateInfo info : errorList) {
if (info.condition
== ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
Log.e(TAG, "ANR detected in process: " + info.processName);
// 二次确认ANR,用于ANR记录或者上报
// ...
}
}
}
}
}
当二次 ANR 确认完成后,我们就可以进一步确认是否要删除本地误捕获的 ANR 日志,或者保留本地的 ANR 日志用于后续上传到服务端。除了上面介绍的方案, ANR 检测的方案还有不少,比如检测 ANR 弹窗,或者通过主线程检测函数延迟等多种方式,但是在本章中介绍的通过信号捕获再通过二次确认的方案,在成功率和性能上,往往都是最优的方案,如果不使用信号捕获的方案,我们就只能通过轮询的方式来检测 ANR,这些方式对性能的损耗自然是比较大的。
3 抓取 Trace 文件
当我们通过前面的方案捕获到 ANR 发送时,那么最重要的事情便是抓取 ANR 的 Trace 日志。我们知道 /data/anr/traces.txt 文件可谓是 ANR 分析利器,这个文件的内容非常全面,包括了所有线程的各种状态、锁和堆栈信息,对于 ANR 问题排查非常有帮助。但是应用程序是没有该文件的访问权限的,所以线上程序想通过直接拿到这个文件去获取 ANR 的日志信息是没法实现的,虽然我们没法直接获取这个文件,但是可以间接的获取该文件的数据内容。
SignalCatcher 线程在收到 SIGQUIT 信号后会获取各个线程的 Trace 信息,并且通过系统的 write 函数来把 Trace 的数据写入到 /data/anr/traces.txt 文件中。如果我们能够 Hook 住这个 write 方法,就可以获取写入到 traces.txt 文件中的 ANR Trace 数据了。这里又用到了前面学到的 PLT Hook 技术,即在捕捉到 SIGQUIT 信号后,通过 PLT Hook 技术,拦截住 libc.so 库中的 write 函数,这样就能获取到 write 函数中所写入的 Trace 数据内容了。笔者这里通过 bytehook 来实现对 write 函数的拦截 ,实现起来也比较简单,代码如下所示。
void dealAnr(){
bytehook_hook_all(
"libc.so",
"write",
(void *)my_write,
nullptr,
nullptr);
}
在上面自定义的 write 拦截函数,my_write 函数中,就能将 ANR 的数据写入到程序自己的目录中了,我们可以通过 c++ 的 fstream 来完成文件数据的写入操作,代码实现如下:
#include <fstream>
ssize_t my_write(int fd, const void* const buf, size_t count) {
BYTEHOOK_STACK_SCOPE();
if (buf != nullptr) {
char *content = (char *) buf;
std::ofstream file("/data/data/com.example.performance_optimize/example_anr.txt"
, std::ios::app);
if (file.is_open()) {
file << content;
file.close();
}
}
return BYTEHOOK_CALL_PREV(my_write,fd, buf,count);
}
通过上面的方案,如下图所示,可以看到成功的捕获到 ANR 的日志,数据和 /data/anr/traces.txt 文件是完全一样的。在程序下次启动时,就可以将该 ANR 日志上传到服务端后用于后续的 ANR 的分析和修复。
4 使用开源框架
ANR 监控的是稳定性治理中最重要的工作之一,因此有很多开源库实现了这个工作。如腾讯的 Matrix,爱奇艺的 xCrash 等,这些库的实现原理和这里所讲的都是类似的,但是经过了大量的用户验证,因此有足够的稳定性和性能的保障,除了 ANR 监控,这些开源库也都会将 Java Crash,Native Crash 等监控整合到一起,形成一套完整的监控工具。读者可以自己去了解这些开源库的优缺点,如稳定性,用户量,更新频率等,并根据业务场景,选择一个合适的开源库库来使用。