Android性能优化系列-腾讯matrix卡顿优化之ANR监控-SignalAnrTracer源码分析

1,257 阅读10分钟

这是性能优化系列之matrix框架的第12篇文章,我将在性能优化专栏中对matrix apm框架做一个全面的代码分析,性能优化是Android高级工程师必知必会的点,也是面试过程中的高频题目,对性能优化感兴趣的小伙伴可以去我主页查看所有关于matrix的分享。

前言

在上一篇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的时候会弹窗提示应用无响应,那么在此之前系统都做了哪些处理?简单来说,分为如下几步:

  1. 收集所有相关的进程,拿到它们的进程id,为后边dump进程信息作准备。
  2. AMS开始按照第1步得到的进程顺序依次dump每个进程的堆栈。
  3. AMS开始dump后,流程会进入Debug.dumpJavaBacktraceToFileTimeout方法中,通过sigqueue方法向需要dump堆栈的进程发送SIGQUIT信号。
  4. 每个进程启动后都会创建一个SignalCatcher线程,当SignalCatcher线程收到SIGQUIT信号时,开始dump自身堆栈。从这里也可以发现,anr发生后,只有dump堆栈的行为会在发生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<SignalHandler*>;
    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的流程。