前言
在上一篇Android性能优化系列-腾讯matrix-TracePlugin卡顿优化之ANR监控LooperAnrTracer源码分析中我们分析了LooperAnrTracer的实现逻辑,从中我们也知道了LooperAnrTracer不是一种严格意义上的anr监控,它基于消息机制,通过一个延时5s的逻辑来操作,5s内消息没有被执行就认为发生了anr,这种监控方式监控anr成功捕获的几率很低,并且真的上报了问题也不能表示应用就一定出现了anr的情况,只能说明出现了卡顿。
今天我们要分析的SignalAnrTracer是严格意义上的anr监控。它基于Linux的信号机制,通过对SIGQUIT信号的监听,再加上一些辅助性的验证逻辑,实现了一个完善的ANR监控方案,在微信上平稳运行了很长时间,可靠性得到了验证。言归正传,开始进入SignalAnrTracer的源码分析,还是从几个关键方法入手:
- 构造方法
- onStartTrace
- onStopTrace
构造方法
构造方法接收了config配置,拿到了两个路径。sAnrTraceFilePath、sPrintTraceFilePath,这两个路径是为写入anr信息而准备的。
public SignalAnrTracer(TraceConfig traceConfig) {
hasInstance = true;
sAnrTraceFilePath = traceConfig.anrTraceFilePath;
sPrintTraceFilePath = traceConfig.printTraceFilePath;
}
onStartTrace
onStartTrace会调用到onAlive方法。onAlive方法首先调用nativeInitSignalAnrDetective传入初始化时的两个path,进入native层初始化监控逻辑;然后调用了AppForegroundUtil的init。
@Override
protected void onAlive() {
super.onAlive();
if (!hasInit) {
nativeInitSignalAnrDetective(sAnrTraceFilePath, sPrintTraceFilePath);
AppForegroundUtil.INSTANCE.init();
hasInit = true;
}
}
可以看到SignalAnrTrace的静态代码块中加载了trace-canary,所以最终可以在matrix-trace-canary模块中找到MatrixTracer.cc这个类,nativeInitSignalAnrDetective方法就在MatrixTracer.cc中。
static {
System.loadLibrary("trace-canary");
}
nativeInitSignalAnrDetective
方法最终会进入AnrDumper的构造,并传入两个路径进去。
static void nativeInitSignalAnrDetective(JNIEnv *env, jclass, jstring anrTracePath, jstring printTracePath) {
const char* anrTracePathChar = env->GetStringUTFChars(anrTracePath, nullptr);
const char* printTracePathChar = env->GetStringUTFChars(printTracePath, nullptr);
//保存了两个路径
anrTracePathString = std::string(anrTracePathChar);
printTracePathString = std::string(printTracePathChar);
//创建包含的类对象。如果在调用之前已经包含一个值,则通过调用其析构函数来销毁包含的值。
sAnrDumper.emplace(anrTracePathChar, printTracePathChar);
}
AnrDumper
AnrDumper在初始化的时候会对SIGQUIT信息进行操作,将SIGQUIT信号设置成UNBLOCK,以确保matrxi创建的SignalHandler可以先拿到SIGQUIT信号。
AnrDumper::AnrDumper(const char* anrTraceFile, const char* printTraceFile) {
// must unblock SIGQUIT, otherwise the signal handler can not capture SIGQUIT
mAnrTraceFile = anrTraceFile;
mPrintTraceFile = printTraceFile;
sigset_t sigSet;
//是将sigSet的信号集先清空
sigemptyset(&sigSet);
//把SIGQUIT加入到sigSet的信号集中
sigaddset(&sigSet, SIGQUIT);
//将SIGQUIT设置成SIG_UNBLOCK。Android默认把SIGQUIT信号设置成BLOCKED,所以只会响应
//sigwait,而不会进入我们设置的handler中。所以需要通过pthread_sigmask将SIGQUIT信号
//设置成UNBLOCK,才能进入我们的handler方法。
pthread_sigmask(SIG_UNBLOCK, &sigSet , &old_sigSet);
}
科普一下系统anr发生的流程,我们知道当Android系统发现anr的时候会弹窗提示应用无响应,那么在此之前系统都做了哪些处理?简单来说,分为如下几步:
看下SignalCatcher的实现。其中WaitForSignal方法调用了sigwait方法,这是一个阻塞方法。这里的死循环,就会一直不断的等待监听SIGQUIT信号的到来。
void* SignalCatcher::Run(void* arg) {
SignalSet signals;
signals.Add(SIGQUIT);
while (true) {
int signal_number = signal_catcher->WaitForSignal(self, signals);
switch (signal_number) {
case SIGQUIT:
signal_catcher->HandleSigQuit();
break;
}
}
}
上边简单描述了anr发生的流程。此时我们再回过头来看一下AnrDumper中的操作,通过调用pthread_sigmask将SIGQUIT设置成SIG_UNBLOCK的目的是什么?
Linux系统提供了两种监听信号的方法,一种是SignalCatcher线程使用的sigwait方法进行同步、阻塞地监听,另一种是使用sigaction方法注册signal handler进行异步监听。当存在两个线程通过sigwait方法监听同一个信号时,哪个线程能收到信号这一点是不确定的,所以matrxi采用的是第二种,注册signal handler异步监听。此时系统中存在两个监听SIGQUIT信号的逻辑,一个是进程的SignalCatcher线程,一个是matrix自己的线程,但是此时matrix线程仍然不能收到SIGQUIT信号,原因在于Android系统默认将SIGQUIT信号设置成了BLOCKED(信号被屏蔽,其他线程无法收到),所以只会响应sigwait,于是就有了AnrDumper中的一步操作,将SIGQUIT设置成UNBLOCKED(解除对SIGQUIT信号的屏蔽),这样一来,matrix注册的signal handler就可以收到SIGQUIT信号。
SignalHandler
在进行UNBLOCKED的设置之后似乎AnrDumper的逻辑就结束了,此时我们再进入其父类SignalHandler中,AnrDumper是继承自SignalHandler的,再看下SignalHandler的构造方法:
SignalHandler::SignalHandler() {
if (!sHandlerStack)
sHandlerStack = new std::vector;
installHandlersLocked();
sHandlerStack->push_back(this);
}
installHandlersLocked
通过sigaction注册signal handler处理函数。sigaction()的功能是为信号指定相关的处理程序,但是它在执行信号处理程序时,会把当前信号加入到进程的信号屏蔽字中,从而防止在进行信号处理期间信号丢失。
sigaction结构体参数分析:
- sa_sigaction:指定的处理函数。
- sa_flags:位掩码,指定用于控制信号处理过程的各种选项。
SA_SIGINFO:调用信号处理器程序时携带了额外参数,其中提供了关于信号的深入信息
SA_RESTART:执行信号处理后自动重启动先前中断的系统调用
SA_RESTART用于控制信号的自动重启动机制,对signal(),Linux默认会自动重启动被中断的系统调用,而对于 sigaction(),Linux默认并不会自动重启动,所以如果希望执行信号处理后自动重启动先前中断的系统调用,就需要为sa_flags指定SA_RESTART标志。
如果改为act.sa_flags = (SA_SIGINFO|SA_RESETHAND),信号处理一次就会退出。
bool SignalHandler::installHandlersLocked() {
struct sigaction sa{};
//设置处理函数,如果设置了SA_SIGINFO标志位,则会使用sa_sigaction处理函数,否则使用sa_handler处理函数。
sa.sa_sigaction = signalHandler;
//位掩码,指定用于控制信号处理过程的各种选项,这里使用SA_RESTART执行信号处理后自动重启到先前中断的系统调用,可以多次捕捉信号
sa.sa_flags = SA_ONSTACK | SA_SIGINFO | SA_RESTART;
if (sigaction(TARGET_SIG, &sa, nullptr) == -1) {
return false;
}
}
signalHandler
signalHandler监听设置之后,就准备好监听SIGQUIT信号了,当信号到来时,开始处理信号,信号的处理分为两种情况,一种是SIGQUIT信号由当前进程发出,一种是SIGQUIT信号由其他进程发出,两种情况都会开启线程。
void AnrDumper::handleSignal(int sig, const siginfo_t *info, void *uc) {
int fromPid1 = info->_si_pad[3];
int fromPid2 = info->_si_pad[4];
int myPid = getpid();
bool fromMySelf = fromPid1 == myPid || fromPid2 == myPid;
if (sig == SIGQUIT) {
pthread_t thd;
//SIGQUIT信号是否是当前进程发出的
if (!fromMySelf) {
pthread_create(&thd, nullptr, anrCallback, nullptr);
} else {
pthread_create(&thd, nullptr, siUserCallback, nullptr);
}
pthread_detach(thd);
}
}
anrCallback
非当前进程发出的SIGQUIT信号。这种情况需要进一步校验是否的确发生了ANR,会进入Java层,通过主线程的消息队列来判断是否的确发生了anr;另外如果指定了anr文件路径,会通过hook系统函数的方式拦截anr信息写入的操作,从而将anr写入到指定文件中;最后再将SIGQUIT信号转发给系统的SignalCatcher线程,使它可以正常完成anr的处理流程。
static void *anrCallback(void* arg) {
//调用Java层的SignalAnrTracer类中的onANRDumped方法,收集部分系统信息,并且会
//专门判断一下主线程的情况,以确认是否的确是发生了卡顿问题
anrDumpCallback();
//如果制定了mAnrTraceFile路径,说明调用方希望能将trace文件写入到这里,则hook写入方法
if (strlen(mAnrTraceFile) > 0) {
hookAnrTraceWrite(false);
}
//SIGQUIT信号被matrix捕获,会导致系统的SignalHandler无法收到信号,这里要转发出去
sendSigToSignalCatcher();
return nullptr;
}
anrDumpCallback
调用Java层的SignalAnrTracer类中的onANRDumped方法,收集部分系统信息,并且会专门判断一下主线程的情况,以确认是否的确是发生了卡顿问题。
如何判断主线程是否卡住的?
反射拿到消息队列MessageQueue中的第一条消息mMessages,并获取到这条消息上的when,when表示消息预期执行的时间,当主线程发生卡顿时,这条消息就无法被及时执行,此时用它的when减去当前时间就会得到一个负值,这个负值的绝对值越大,就说明卡住的时间越长。然后matrix用这个差值和FOREGROUND_MSG_THRESHOLD(前台卡住时常-2000)、BACKGROUND_MSG_THRESHOLD(后台卡住时常-10000)比较大小,如果差值小于定义值,就说明主线程当前消息已经被卡住未执行了,如此就进一步验证的确发生了anr,开始组装信息上报。
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;
//BACKGROUND_MSG_THRESHOLD = -10000毫秒也就是-10s
long timeThreshold = BACKGROUND_MSG_THRESHOLD;
if (currentForeground) {
timeThreshold = FOREGROUND_MSG_THRESHOLD;
}
return time < timeThreshold;
}
} catch (Exception e) {
return false;
}
return false;
}
hookAnrTraceWrite
这里主要hook了这么几个方法:
- connect(api 27以下是open)
- write
connect和open方法主要是为了在socket打开获取到SignalCatcher的线程id,并将isTraceWrite设置为true。
当开始写入时,拦截write方法,将anr文件同时写一份到指定路径下,写入的时候是进入了writeAnr方法
ssize_t my_write(int fd, const void* const buf, size_t count) {
if(isTraceWrite && gettid() == signalCatcherTid) {
isTraceWrite = false;
signalCatcherTid = 0;
if (buf != nullptr) {
std::string targetFilePath;
if (fromMyPrintTrace) {
targetFilePath = printTracePathString;
} else {
targetFilePath = anrTracePathString;
}
if (!targetFilePath.empty()) {
char *content = (char *) buf;
//写入指定文件
writeAnr(content, targetFilePath);
if(!fromMyPrintTrace) {
anrDumpTraceCallback();
} else {
printTraceCallback();
}
fromMyPrintTrace = false;
}
}
}
//原有的写入逻辑
return original_write(fd, buf, count);
}
sendSigToSignalCatcher
重新将SIGQUIT信号发送给进程的SignalCatcher线程,以恢复系统的anr处理流程。
static void sendSigToSignalCatcher() {
int tid = getSignalCatcherThreadId();
syscall(SYS_tgkill, getpid(), tid, SIGQUIT);
}
siUserCallback
当前进程发出的信号。这种情况可以直接确认当前进程发生了ANR,然后hook系统write方法将anr信息写入指定文件。
static void *siUserCallback(void* arg) {
if (strlen(mPrintTraceFile) > 0) {
hookAnrTraceWrite(true);
}
sendSigToSignalCatcher();
return nullptr;
}
onStopTrace
onStopTrace会调用到onDead方法
@Override
protected void onDead() {
super.onDead();
nativeFreeSignalAnrDetective();
}
调用reset方法,会执行AnrDumper、SignalHandler的析构方法释放资源,详细的就不深入看了。
static void nativeFreeSignalAnrDetective(JNIEnv *env, jclass) {
sAnrDumper.reset();
}
总结
SignalAnrTrace的核心功能是基于Linux的信号机制,总结一下SignalAnrTrace的逻辑:
- 底层设置对SIGQUIT信号的监听。
- 监听到SIGQUIT信号后再结合主线程的执行状态进一步确认ANR的发生。
- hook ANR的写入时机,拦截write方法,从而将ANR trace信息写入指定文件。
- 转发SIGQUIT信号给进程的SignalHandler,继续完成系统ANR的流程。