谈到Android中的anr监控,一般比较可靠是通过监听系统给进程发送的SIGQUIT信号,我们可以使用sigaction函数增加SIGQUIT信号的捕获,然后在信号处理函数中判断SIGQUIT做响应的处理,监听信号如下代码:
1
|
sigaction(SIGQUIT, &sa, &old_sa[SIGILL]);
|
该方法在signal.h中的方法,方法定义如下:
1
|
int sigaction(int __signal, const struct sigaction* __new_action, struct sigaction* __old_action);
|
- 第一个参数是信号类型,因此ANR是监听SIGQUIT信号,所以此处直接给该信号。
- 第二个参数是新安装的信号处理行为(即告诉系统以后这个信号怎么处理)。
- 第三个参数是用来保存原来的信号处理行为(可选,如果你想之后恢复的话)。
这里着重看下sigaction被struct修饰的结构体的定义:
1
2
3
4
5
6
7
8
9
10
|
#define __SIGACTION_BODY \
int sa_flags; \
union { \
sighandler_t sa_handler; \
void (*sa_sigaction)(int, struct siginfo*, void*); \
}; \
sigset_t sa_mask; \
void (*sa_restorer)(void); \
struct sigaction { __SIGACTION_BODY };
|
- sa_flags:行为控制的标志,比如SA_RESTART、SA_SIGINFO等。
- SA_RESTART:被信号中断的系统调用自动重启。
- SA_SIGINFO:使用sa_sigaction替代sa_handler。
- SA_ONSTACK:使用备用信号栈。SA_NODEFER:不屏蔽当前信号。
- sa_handler:信号处理函数
- sa_sigaction:带上下文的高级处理函数(与SA_SIGINFO配合使用)
- sa_mask:当处理信号时,临时屏蔽哪些信号
如果只是按照上面的sigaction函数监听SIGQUIT信号的时候,会发现处理函数是捕获不到SIGQUIT信号,那是因为系统屏蔽了SIGQUIT信号,不允许sigaction接受SIGQUIT信号。
程序启动的时候会启动一个SignalCatcher线程,该线程会通过sigwait函数阻塞监听SIGQUIT信号。sigwait函数也是监听信号的函数,相比于sigaction函数,它是同步接收的方式,也就是只允许一个地方监听指定的信号,而sigaction函数是异步的,可以在多个地方都监听之心信号并进行处理。

虽然系统屏蔽了 sigaction 异步接收 SIGQUIT 信号的方式,但是没法屏蔽通过 sigwait 同步监听 SIGQUIT 信号 ,这样也保障了 SignalCatcher 线程接收到 SIGQUIT 信号后,能够正常的获取进程中的各个线程的信息,并输出到 /data/anr/traces.txt 文件中。
虽然 SIGQUIT 信号被系统屏蔽了,但是我们可以使用 pthread_sigmask 函数将 SIGQUIT 信号从当前线程的信号屏蔽集中移除,实现如下:
1
2
3
4
5
6
7
|
sigset_t new_set;
// 初始化清空信号集
sigemptyset(&new_set);
// 将 SIGQUIT 信号添加到信号集
sigaddset(&new_set, SIGQUIT);
// 将当前线程的信号屏蔽集设置为信号集的补集,即解除 SIGQUIT 信号的屏蔽
pthread_sigmask(SIG_UNBLOCK, &new_set, &old_set);
|
解除对SIGQUIT信号的屏蔽后,下面就能捕获到SIGQUIT信号了,然后我们在信号的处理函数中判断是SIGQUIT信号来对ANR的处理,并捕获ANR的trace数据,还需要保证原来的SignalCatcher线程能响应SIGQUIT信号的,这里我们想下,既然上面解除了当前线程接收到SIGQUIT信号后,那么当前线程会拦截掉了SIGQUIT信号,那么SignalCatcher线程就接收不到SIGQUIT信号了,所以需要给SignalCatcher补发一次SIGQUIT信号,因为SignalCatcher线程的主要职责如下:
- 注册 SIGQUIT 的 handler;
- 当系统(如 system_server)向 app 进程发送 SIGQUIT 时,负责 dump 所有线程的堆栈;
- 输出 trace 到 /data/anr/traces.txt。
由于SignalCatcher 线程不是通过 sigaction 来响应 SIGQUIT 信号的,所以我们直接执行 old_sa 是没法生效的,此时可以通过 tgkill 信号发生函数 tgkill,往 SignalCatcher 线程发生一个 SIGQUIT 信号,tgkill 函数往指定线程发送信号时需要知道线程的 id,所以我们还需要通过遍历 /proc/{pid}/task 目录下所记录的该进程下所有的线程数据,拿到名称为 “SignalCatcher” 线程对应的线程 id。代码实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
int getSignalCatcherThreadId() {
std::string proc_path = "/proc/" + std::to_string(getpid()) + "/task";
DIR *dir = opendir(proc_path.c_str());
if (dir == nullptr) return -1;
struct dirent *entry;
while ((entry = readdir(dir)) != nullptr) {
std::string name = entry->d_name;
if (std::all_of(name.begin(), name.end(), ::isdigit)) {
std::string status_path = proc_path + "/" + name + "/status";
std::ifstream status_file(status_path);
if (!status_file.is_open()) continue;
std::string line;
while (std::getline(status_file, line)) {
if (line.find("Name:\tSignalCatcher") != std::string::npos) {
int tid = std::stoi(name);
closedir(dir);
return tid;
}
}
}
}
closedir(dir);
return -1;
}
int tid = getSignalCatcherThreadId();
if (tid != -1) {
tgkill(getpid(), tid, SIGQUIT); // 转发给 SignalCatcher 线程
}
|
上面就完成了当前线程监听SIGQUIT信号,虽然完成了SIGQUIT信号的监听,但是不能保证是当前进程出现了ANR,因为有可能其他应用发生ANR时,cpu使用率占用比较高的进程也会收到SIGQUIT信号。其他进程或者线程也可以手动调用tgkill函数发送SIGQUIT信号给当前进程,因此我们还需要二次确认当前进程发生了ANR,二次确认的方案一般有下面两种:
- 当进程发生ANR的时候,在ActivityManagerService通知进程启动ANR弹框前,会给发生了ANR的进程设置一个
NOT_RESPONDING的标志位,表示该进程发生了ANR,而这个标志位可以通过ActivityManager的getProcessesInErrorState方法来获取,思路就是在上面监听到SIGQUIT信号后,通过jni方法回调到java层,然后在java层判断这个标志位判断是不是当前进程发生了ANR,来进行二次确认。
- 我们都知道主线程中消息队列的消息是按照时间先后的顺序进行排列,每次都会取队列的最开始的消息,每个消息是通过变量when放到队列的,所以我们可以判断消息队列中的开始消息when和当前时间对比,如果超过我们设置的阈值,就认为发生了anr。
关于上面两种方案各有利弊,第一种方案会存在漏掉,因为存在一些后台的ANR不会通过ActivityManagerService给进程设置一个NOT_RESPONDING的标志位,因为后台的ANR会直接杀死进程,此时都没机会捕捉该标志位了。还有种情况是闪退ANR,相当一部分机型(例如OPPO、VIVO两家的高Android版本的机型)修改了ANR的流程,即使是发生在前台的ANR,也不会弹框,而是直接杀死进程,也就是闪退。这部分的机型覆盖的用户量也非常大。并且,确定两家今后的新设备会一直维持这个机制。所以基于此,第二种方案更加全面,但是第二种方案也会存在弊端,它需要版本的兼容,因为在低版本中无法获取到messageQueue,并且是通过反射的手段来进行获取。
第一种方案(获取标志位)实现代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//jni回调方法
void anrDumpTraceCallback() {
if (g_vm == nullptr || g_utils_class == nullptr) return;
JNIEnv *env = nullptr;
if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) return;
jmethodID method = env->GetStaticMethodID(g_utils_class, "onANRDumpTrace", "()V");
if (method != nullptr) {
env->CallStaticVoidMethod(g_utils_class, method);
}
g_vm->DetachCurrentThread();
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
static void onANRDumpTrace() {
Log.d("MyUtils", "onANRDumpTrace start");
ActivityManager am = (ActivityManager) getSystemService(context, ActivityManager.class);
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("MyUtils", "ANR detected in process: " + info.processName);
// 二次确认ANR,用于ANR记录或者上报
// ...
}
}
}
}
}
|
第二种方案代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@RequiresApi(api = Build.VERSION_CODES.M)
private static boolean isMainThreadBlocked() {
try {
MessageQueue mainQueue = Looper.getMainLooper().getQueue();
Field field = mainQueue.getClass().getDeclaredField("mMessages");
field.setAccessible(true);
final Message mMessage = (Message) field.get(mainQueue);
if (mMessage != null) {
anrMessageString = mMessage.toString();
long when = mMessage.getWhen();
if (when == 0) {
return false;
}
long time = when - SystemClock.uptimeMillis();
anrMessageWhen = time;
long timeThreshold = BACKGROUND_MSG_THRESHOLD;
if (currentForeground) {
timeThreshold = FOREGROUND_MSG_THRESHOLD;
}
return time < timeThreshold;
} else {
MatrixLog.i(TAG, "mMessage is null");
}
} catch (Exception e) {
return false;
}
return false;
}
|
在二次确认后,还需要对trace文件进行获取,trace文件会存储在/data/anr/traces.txt,文件的内容非常全面,包含了所有线程的各种状态、锁和堆栈,但是很不幸的是应用程序没有权限获取到该文件,所以我们需要间接获取该文件的数据内容。在上面提到的SignalCatcher线程收到SIGQUIT信号后,会收集各个线程的trace信息,并通过系统的write方法把trace的数据写到/data/anr/traces.txt文件中。如果我们能够Hook住这个write方法,就可以获取写入到traces.txt文件的内容了。这里我们使用PLT Hook技术,拦截住该write方法,就可以获取写入到traces.txt文件的内容。这里的write方法是libc.so库中,此处使用bhook拦截住该库的方法就能获取到该trace内容了,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
void dealAnr() {
bytehook_hook_all(
"libc.so",
"write",
(void *) my_write,
nullptr,
nullptr);
}
ssize_t my_write(int fd, const void *const buf, size_t count) {
BYTEHOOK_STACK_SCOPE();
if (buf != nullptr) {
std::string content((const char *) buf, count);
if (content.find("Cmd line") != std::string::npos) {
LOGI("Detected ANR trace write. Triggering callback...");
anrDumpTraceCallback(); // 回调 Java 方法
}
std::ofstream file("/data/data/com.example.nativelib/example_anr.txt", std::ios::app);
if (file.is_open()) {
file << content;
file.close();
}
}
return BYTEHOOK_CALL_PREV(my_write, fd, buf, count);
}
|
当进程发生anr后,会在上面的目录中生成example_anr.txt文件,我们来测试一波:

从上面日志也能看出来主线程出现了sleep状态,阻塞了主线程的,并且对应有堆栈行号。上面说的二次确认中目前试了android13和15都不太行,ActivityManager的getProcessesInErrorState方法已经不对外提供出错的进程信息了,并且在android10以上不让获取messageQueue中的messages了。所以对应anr的二次确认已经算是一个阉割了吧,只能获取trace日志了。
参考: