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

2023年 9月 28日 48.8k 0

前言

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

  • 收集所有相关的进程,拿到它们的进程id,为后边dump进程信息作准备。
  • AMS开始按照第1步得到的进程顺序依次dump每个进程的堆栈。
  • AMS开始dump后,流程会进入Debug.dumpJavaBacktraceToFileTimeout方法中,通过sigqueue方法向需要dump堆栈的进程发送SIGQUIT信号。
  • 每个进程启动后都会创建一个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;
        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的流程。

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论