美文网首页性能程序员
Android 性能监控框架 xCrash-捕获 Java 和

Android 性能监控框架 xCrash-捕获 Java 和

作者: Android阿南 | 来源:发表于2020-11-25 20:57 被阅读0次

    概述

    xCrash 是爱奇艺开源的一个用于监控 Java 和 Native 崩溃的组件,只需要在 Application 类中初始化即可启用:

    XCrash.init(this);
    复制代码
    

    也可以对一些选项进行配置:

    XCrash.InitParameters params = new XCrash.InitParameters();
    
    // 通用配置
    params.setAppVersion(mConfig.getAppVersion())
            .setPlaceholderCountMax(DEFAULT_PLACEHOLDER_MAX_COUNT)
            .setPlaceholderSizeKb(DEFAULT_PLACEHOLDER_SIZE_KB)
            .setLogFileMaintainDelayMs(DEFAULT_LOG_FILE_MAINTAIN_DELAY_MS);
    
    // Java 崩溃配置
    params.setJavaRethrow(true)
            .setJavaDumpFds(false)
            // ...
            .setJavaCallback(callback);
    
    // Native 崩溃配置
    params.setNativeRethrow(true)
            // ...
            .setNativeCallback(callback);
    
    // ANR 配置
    params.setAnrCallback(callback);
    
    XCrash.init(mConfig.getApp(), params);
    复制代码
    

    崩溃发生后的 json 示例如下:

    {
        "logcat":"...",
        "java stacktrace":"...",
        "Brand":"vivo",
        "Model":"vivo Y66",
        "pid":"18227",
        "network info":"...",
        "memory info":"...",
        "App version":"1.2.3-beta456-patch789",
        "tname":"main ",
        "pname":"xcrash.sample",
        "Manufacturer":"vivo",
        "Rooted":"No",
        "open files":"...",
        "other threads":"...",
        "OS version":"6.0.1",
        "ABI list":"armeabi-v7a,armeabi",
        "Start time":"2020-06-01T14:47:11.768+0800",
        "foreground":"yes",
        "tid":"18227",
        "Build fingerprint":"vivo\/PD1621\/PD1621:6.0.1\/MMB29M\/compiler04111924:user\/release-keys",
        "App ID":"xcrash.sample",
        "Crash type":"java",
        "API level":"23",
        "Crash time":"2020-06-01T14:47:36.029+0800",
        "Tombstone maker":"xCrash 2.4.9"
    }
    复制代码
    

    可自定义回调处理逻辑,比如将信息上报给服务器:

    ICrashCallback callback = new ICrashCallback() {
        @Override
        public void onCrash(String logPath, String emergency) {
            if (emergency != null) {
                sendReport(logPath, emergency);
            }
        }
    }
    
    private void sendReport(String logPath, String emergency) {
        // 解析日志文件,生成 json 报告
        Map<String, String> map = TombstoneParser.parse(logPath, emergency);
        String crashReport = new JSONObject(map).toString();
    
        // 发送到服务器
        // ...
    
        // 删除日志文件
        TombstoneManager.deleteTombstone(logPath);
    }
    复制代码
    

    捕获 Java 崩溃

    Java 崩溃的捕获很简单,xCrash 是通过 UncaughtExceptionHandler 接口实现的,处理逻辑如下:

    class JavaCrashHandler implements UncaughtExceptionHandler {
    
        void initialize(...) {
            // 获取原来的 handler
            this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
            Thread.setDefaultUncaughtExceptionHandler(this);
        }
    
        @Override
        public void uncaughtException(Thread thread, Throwable throwable) {
            if (defaultHandler != null) {
                Thread.setDefaultUncaughtExceptionHandler(defaultHandler);
            }
    
            // 处理崩溃
            handleException(thread, throwable);
    
            if (this.rethrow) { // 如果 rethrow 为 true,那么将异常抛给原 Hanlder
                if (defaultHandler != null) {
                    defaultHandler.uncaughtException(thread, throwable);
                }
            } else { // 否则退出应用
                ActivityMonitor.getInstance().finishAllActivities();
                Process.killProcess(this.pid);
                System.exit(10);
            }
        }
    }
    复制代码
    

    出现崩溃后,JavaCrashHandler 会收集 logcat、异常堆栈、文件句柄、内存等信息,并写入到 tombstone 文件中。

    值得注意的是,xCrash 会预先创建多个 placeholder 文件,在出现崩溃后再重命名为 tombstone 文件:

    File createLogFile(String filePath) {
        File newFile = new File(filePath);
    
        File dir = new File(logDir);
        File cleanFile = dir.listFiles()[cleanFilesCount - 1];
        if (cleanFile.renameTo(newFile)) {
            return newFile;
        }
    
        newFile.createNewFile();
        return newFile;
    }
    复制代码
    

    这么做可以避免文件句柄不足导致无法创建日志文件。

    获取崩溃信息

    Java 堆栈

    对于 Java 堆栈,xCrash 是通过下面两个方法来获取的:

    Throwable.printStackTrace()
    Thread.getAllStackTraces()  // 获取其它线程堆栈
    复制代码
    

    根据 Android 开发高手课的说法,Thread.getAllStackTraces() 的优点是简单、兼容性好,缺点是成功率不高、需要暂停线程,而且 7.0 之后无法通过获取主线程堆栈。

    logcat

    对于 Logcat 日志的获取,xCrash 是通过 Logcat 命令行工具实现的:

    static String getLogcat(int logcatMainLines, int logcatSystemLines, int logcatEventsLines) {
        int pid = android.os.Process.myPid();
        StringBuilder sb = new StringBuilder();
    
        getLogcatByBufferName(pid, sb, "main", logcatMainLines, 'D');
        getLogcatByBufferName(pid, sb, "system", logcatSystemLines, 'W');
        getLogcatByBufferName(pid, sb, "events", logcatSystemLines, 'I');
    
        return sb.toString();
    }
    
    private static void getLogcatByBufferName(int pid, StringBuilder sb, String bufferName, int lines, char priority) {
        List<String> command = new ArrayList<String>();
        command.add("/system/bin/logcat");
        command.add("-b");
        command.add(bufferName);
        command.add("-d");
        command.add("-v");
        command.add("threadtime");
        command.add("-t");
    
        //append logs
        String line;
        Process process = new ProcessBuilder().command(command).start();
        BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
        while ((line = br.readLine()) != null) {
            sb.append(line).append("\n");
        }
    }
    复制代码
    

    根据 Android 开发高手课的说法,这种方式的优点是简单、兼容性好,缺点是可控性差、失败率高。

    文件句柄

    文件句柄信息则是通过读取文件 /proc/self/fd 文件获取的:

    static String getFds() {
        StringBuilder sb = new StringBuilder("open files:\n");
    
        File dir = new File("/proc/self/fd");
        File[] fds = dir.listFiles();
    
        for (File fd : fds) {
            String path = Os.readlink(fd.getAbsolutePath());
            sb.append(fd.getName()).append(": ").append(path.trim()).append('\n');
        }
        return sb.toString();
    }
    复制代码
    

    内存信息

    内存信息则是通过读取文件 /proc/meminfo、/proc/self/status、/proc/self/limits,以及调用系统接口 Debug.getMemoryInfo 获取的:

    static String getMemoryInfo() {
        return "memory info:\n"
            + Util.getFileContent("/proc/meminfo")
            + Util.getFileContent("/proc/self/status")
            + Util.getFileContent("/proc/self/limits")
            + Util.getProcessMemoryInfo()
    }
    
    static String getProcessMemoryInfo() {
        StringBuilder sb = new StringBuilder();
        Debug.MemoryInfo mi = new Debug.MemoryInfo();
        Debug.getMemoryInfo(mi);
        sb.append(mi.getMemoryStat("summary.java-heap"));
        sb.append(mi.getMemoryStat("summary.native-heap"));
        sb.append(mi.getMemoryStat("summary.code"));
        sb.append(mi.getMemoryStat("summary.stack")));
        sb.append(mi.getMemoryStat("summary.graphics")));
        sb.append(mi.getMemoryStat("summary.private-other")));
        sb.append(mi.getMemoryStat("summary.system")));
        sb.append(mi.getMemoryStat("summary.total-pss"));
        sb.append(mi.getMemoryStat("summary.total-swap"));
        return sb.toString();
    }
    复制代码
    

    捕获 Native 崩溃

    初始化

    Native Crash 检测是通过监控系统信号实现的,开始监控之前,首先需要执行一些初始化操作,比如,因为需要将崩溃信息写到文件里,而考虑到文件句柄耗尽的情况,可以提前获取 2 个文件句柄(一个给 crash 事件,一个给 anr 事件):

    int xc_common_init(...) {
        //create prepared FD for FD exhausted case
        xc_common_open_prepared_fd(1);
        xc_common_open_prepared_fd(0);
    }
    复制代码
    

    同时,设置 Java 回调,以便在崩溃时将信息传到 Java 层:

    static void xc_crash_init_callback(JNIEnv *env) {
        xc_crash_cb_method = (*env)->GetStaticMethodID(env, xc_common_cb_class, "crashCallback", "...");
    }
    复制代码
    

    然后启动后台线程,创建 eventfd 并监听,如果收到了 eventfd 消息就说明发生了崩溃,那时再由子线程进行处理:

    static void xc_crash_init_callback(JNIEnv *env) {
        //eventfd and a new thread for callback
        xc_crash_cb_notifier = eventfd(0, EFD_CLOEXEC);
        pthread_create(&xc_crash_cb_thd, NULL, xc_crash_callback_thread, NULL);
    }
    复制代码
    

    个人理解,开启子线程是为了获取 env 变量,否则无法回调 Java 层。而使用 eventfd 而不是条件变量或其它同步方式,是因为 eventfd 更轻量级,性能损耗更小,对崩溃处理造成的影响更小。

    注册信号处理器

    接着,使用 sigaction 接口注册信号处理器,监控系统信号:

    static xcc_signal_crash_info_t xcc_signal_crash_info[] =
            {
                    {.signum = SIGABRT},   // 程序异常终止
                    {.signum = SIGBUS},    // 非法地址
                    {.signum = SIGFPE},    // 算术运算出错
                    {.signum = SIGILL},    // 非法指令
                    {.signum = SIGSEGV},   // 非法访问(没有权限的)内存
                    {.signum = SIGTRAP},   // 断点指令或其它 trap 指令
                    {.signum = SIGSYS},    // 非法的系统调用
                    {.signum = SIGSTKFLT}  // 栈溢出
            };
    
    int xcc_signal_crash_register(void (*handler)(int, siginfo_t *, void *)) {
        struct sigaction act;
        memset(&act, 0, sizeof(act));
        act.sa_sigaction = handler;
    
        // 注册信号处理器,并保存旧处理器,以便在处理器完毕后,重新将信号发送给旧处理器
        size_t i;
        for (i = 0; i < sizeof(xcc_signal_crash_info) / sizeof(xcc_signal_crash_info[0]); i++)
            sigaction(xcc_signal_crash_info[i].signum, &act, &(xcc_signal_crash_info[i].oldact));
    
        return 0;
    }
    复制代码
    

    写入崩溃信息到文件

    监听到信号发生时,就认为发生了崩溃,打开日志文件夹(如果打开失败,就使用之前预留的 fd):

    static int xc_common_open_log(...) {
        if ((fd = open(xc_common_log_dir, flags))) < 0) {
            //try again with the prepared fd
            xc_common_close_prepared_fd(is_crash);
            fd = open(xc_common_log_dir, flags);
        }
    }
    复制代码
    

    将 placeholder 重命名文件为 tombstone 文件,准备记录崩溃信息:

    //try to rename a placeholder file and open it
    while ((n = syscall(XCC_UTIL_SYSCALL_GETDENTS, fd, buf, sizeof(buf))) > 0) { // 遍历文件夹
        if (0 == rename(placeholder_pathname, pathname)) {
            close(fd);
            return open(pathname, flags);
        }
    }
    复制代码
    

    为了防止二次崩溃导致日志写入失败,启动一个新的进程,将堆栈、内存、文件句柄等信息写入到 tombstone 文件中:

    static void xc_crash_signal_handler(int sig, siginfo_t *si, void *uc) {
        ... // 先收集崩溃环境信息,比如时间戳、线程名等
        pid_t dumper_pid = xc_crash_fork(xc_crash_exec_dumper); // 创建新进程
        waitpid(dumper_pid, &status, __WALL); // 等待进程执行完毕
    }
    复制代码
    

    回调 Java 层

    文件写入完毕后,发送信息给之前创建的 eventfd:

    static void xc_crash_signal_handler(int sig, siginfo_t *si, void *uc) {
        xc_crash_callback();
    }
    
    static void xc_crash_callback() {
        write(xc_crash_cb_notifier, &data, sizeof(data);
        pthread_join(xc_crash_cb_thd, NULL); // 等待线程执行完毕
    }
    复制代码
    

    子线程读取到 eventfd 消息后,回调给 Java 层:

    static void *xc_crash_callback_thread(void *arg) {
        read(xc_crash_cb_notifier, &data, sizeof(data));
        //do callback
        (*env)->CallStaticVoidMethod(env, ...);
    }
    复制代码
    

    之后 Java 层的 ICrashCallback 再进行具体的崩溃处理,比如发送 json 数据给服务器。

    重抛异常

    和 Java Crash 一样,Native Crash 也有重抛异常的机制,实现原理很简单,取消注册自己的信号处理器,注册之前保存的旧信号处理器,xCrash 处理完毕后,重新将信号发送出去,由旧信号处理器处理:

    static void xc_crash_signal_handler(int sig, siginfo_t *si, void *uc) {
        // 取消注册自己的信号处理器,注册之前保存的旧信号处理器
        if (xc_crash_rethrow) {
            xcc_signal_crash_unregister();
        }
    
        ...    // 获取崩溃日志
    
        // 回调给 Java 层
        xc_crash_callback();
    
        // 将信号重新发送出去,交给旧处理器处理
        xcc_signal_crash_queue(si)
    }
    复制代码
    

    一些问题

    Native 崩溃发生时,部分信息的获取似乎基于 ptrace 实现的,但个人对 ptrace 没有了解,代码也很难看懂,就不分析了。

    根据 Android 开发高手课的说法,崩溃发生时,除了要考虑文件句柄、二次崩溃等问题之外,栈溢出、堆内存耗尽的问题也是必须要考虑的,但 xCrash 似乎没有做这方面的处理,而且堆栈、logcat 日志的获取使用的也是比较简单的方法,所以,对于企业级应用,Bugly 等框架或许才是更好的选择。

    总结

    Java 崩溃监控是通过 UncaughtExceptionHandler 实现的。崩溃发生时,通过 logcat 命令行工具获取 logcat 日志,通过 /proc 文件系统收集文件句柄、内存等信息。

    Native 崩溃检测是通过监控系统信号实现的,流程如下:

    1. 注册自己的信号处理器,保存旧信号处理器
    2. 崩溃发生时,启动新的进程,收集崩溃环境信息,写入到 tombstone 文件, 回调给 Java 层
    3. 注册旧信号处理器,重新发送信号,交给旧信号处理器处理

    值得注意的细节有:

    1. 考虑到文件句柄耗尽的情况,xCrash 会提前获取文件句柄,预先创建多个 placeholder 文件,防止崩溃发生时无法创建文件
    2. 为了防止二次崩溃导致日志生成失败,xCrash 创建了一个新的进程来收集崩溃现场
    3. 用于 Java 回调的子线程使用 eventfd 监听崩溃的发生

    作者:zouzhiheng
    链接:https://juejin.cn/post/6898938662214434830
    来源:掘金

    相关文章

      网友评论

        本文标题:Android 性能监控框架 xCrash-捕获 Java 和

        本文链接:https://www.haomeiwen.com/subject/cthoiktx.html