xCrash 是愛奇藝開源的一個用於監控 Java 和 Native 崩潰的組件,只須要在 Application 類中初始化便可啓用:java
XCrash.init(this);
複製代碼
也能夠對一些選項進行配置:android
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 示例以下:git
{
"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"
}
複製代碼
可自定義回調處理邏輯,好比將信息上報給服務器:github
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 崩潰的捕獲很簡單,xCrash 是經過 UncaughtExceptionHandler 接口實現的,處理邏輯以下:json
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 文件:markdown
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;
}
複製代碼
這麼作能夠避免文件句柄不足致使沒法建立日誌文件。app
對於 Java 堆棧,xCrash 是經過下面兩個方法來獲取的:ide
Throwable.printStackTrace()
Thread.getAllStackTraces() // 獲取其它線程堆棧
複製代碼
根據 Android 開發高手課的說法,Thread.getAllStackTraces() 的優勢是簡單、兼容性好,缺點是成功率不高、須要暫停線程,並且 7.0 以後沒法經過獲取主線程堆棧。工具
對於 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 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); // 等待進程執行完畢
}
複製代碼
文件寫入完畢後,發送信息給以前建立的 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)
}
複製代碼
Java 崩潰監控是經過 UncaughtExceptionHandler 實現的。崩潰發生時,經過 logcat 命令行工具獲取 logcat 日誌,經過 /proc 文件系統收集文件句柄、內存等信息。
Native 崩潰檢測是經過監控系統信號實現的,流程以下:
值得注意的細節有:
PS:才發現,原來官方原本就寫了一篇 xCrash 的文章,有些班門弄斧了,推薦閱讀 blog.itpub.net/69945252/vi…