Crash(應用崩潰)是由于代碼異常而導致 App 非正常退出,導致應用程序無法繼續使用,所有工作都停止的現象。發生 Crash 后需要重新啟動應用(有些情況會自動重啟)。
在 Android 應用中發生的 Crash 有兩種類型,Java 層的 Crash 和 Native 層 Crash。這兩種Crash 的監控和獲取堆棧信息有所不同。
Java Crash
Java 的 Crash 監控比較簡單,Java 中的 Thread 定義了一個接口UncaughtExceptionHandler
;用于處理未捕獲的異常導致線程的終止(注意:catch了的是捕獲不到的),當我們的應用 crash 的時候,就會走 UncaughtExceptionHandler.uncaughtException
,在該方法中可以獲取到異常的信息,我們通過 Thread.setDefaultUncaughtExceptionHandler
該方法來設置線程的默認異常處理器,我們可以將異常信息保存到本地或者是上傳到服務器,方便我們快速的定位問題。
/**
* UncaughtException處理類, 當程序發生Uncaught異常的時候, 有該類來接管程序, 并記錄發送錯誤報告
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String FILE_NAME_SUFFIX = ".trace";
/**
* 系統默認的UncaughtException處理類
*/
private static Thread.UncaughtExceptionHandler mDefaultCrashHandler;
private static Context mContext;
private volatile static CrashHandler mInstance = null;
private CrashHandler() {
}
public static CrashHandler getInstance() {
if (mInstance == null) {
synchronized (CrashHandler.class) {
if (mInstance == null) {
mInstance = new CrashHandler();
}
}
}
return mInstance;
}
public void init(Context context) {
// 獲取系統默認的UncaughtException處理器
//默認為:RuntimeInit#KillApplicationHandler
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
//設置該CrashHandler為程序的默認處理器
Thread.setDefaultUncaughtExceptionHandler(this);
mContext = context.getApplicationContext();
}
/***
* 當程序中有未被捕獲的異常,系統將會調用這個方法
* @param thread 出現未捕獲異常的線程
* @param throwable 得到異常信息
*/
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
try {
//自行處理:保存本地
File file = dealException(thread, throwable);
//上傳服務器
//......
} catch (Exception e) {
e.printStackTrace();
} finally {
//交給系統默認程序處理
if (mDefaultCrashHandler != null) {
// 如果用戶沒有處理則讓系統默認的異常處理器來處理
mDefaultCrashHandler.uncaughtException(thread, throwable);
}
}
}
/**
* 導出異常信息到SD卡
*/
private File dealException(Thread thread, Throwable e) throws Exception {
//存儲位置:sdcard->Android->data->包名->cache->crash_info
File dir = new File(mContext.getExternalCacheDir(), "crash_info");
if (!dir.exists()) {
dir.mkdirs();
}
long timeMillis = System.currentTimeMillis();
File file = new File(dir, timeMillis + ".txt");
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date());
// //往文件中寫入數據
PrintWriter pw = new PrintWriter(new FileWriter(file));
pw.println(time);
pw.println("Thread: " + thread.getName());
pw.println(getPhoneInfo());
//寫入crash堆棧
e.printStackTrace(pw);
Throwable mThrowable = e.getCause();
// 迭代棧隊列把所有的異常信息寫入writer中
while (mThrowable != null) {
mThrowable.printStackTrace(pw);
// 換行 每個個異常棧之間換行
pw.append("\r\n");
mThrowable = mThrowable.getCause();
}
pw.close();
return file;
}
/**
* 記錄手機信息
*/
private String getPhoneInfo() throws PackageManager.NameNotFoundException {
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
StringBuilder sb = new StringBuilder();
//App版本
sb.append("App Version: ");
sb.append(pi.versionName);
sb.append("_");
sb.append(pi.versionCode).append("\n");
//Android版本號
sb.append("OS Version: ");
sb.append(Build.VERSION.RELEASE);
sb.append("_");
sb.append(Build.VERSION.SDK_INT).append("\n");
//手機制造商
sb.append("Vendor: ");
sb.append(Build.MANUFACTURER).append("\n");
//手機型號
sb.append("Model: ");
sb.append(Build.MODEL).append("\n");
//CPU架構
sb.append("CPU: ");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sb.append(Arrays.toString(Build.SUPPORTED_ABIS)).append("\n");
} else {
sb.append(Build.CPU_ABI).append("\n");
}
return sb.toString();
}
}
NDK Crash
相對于 Java 的 Crash,NDK 的錯誤無疑更加讓人頭疼。
Linux信號機制
信號機制是 Linux 進程間通信的一種重要方式,Linux 信號一方面用于正常的進程間通信和同步,另一方面它還負責監控系統異常及中斷。當應用程序運行異常時,Linux內核將產生錯誤信號并通知當前進程。當前進程在接收到該錯誤信號后,可以有三種不同的處理方式。
- 忽略該信號;
- 捕捉該信號并執行對應的信號處理函數(信號處理程序);
- 執行該信號的缺省操作(如終止進程);
當 Linux 應用程序在執行時發生嚴重錯誤,一般會導致程序崩潰。其中,Linux 專門提供了一類 crash 信號,在程序接收到此類信號時,缺省操作是將崩潰的現場信息記錄到核心文件,然后終止進程。
常見崩潰信號列表:
信號 | 描述 |
---|---|
SIGSEGV | 內存引用無效。 |
SIGBUS | 訪問內存對象的未定義部分。 |
SIGFPE | 算術運算錯誤,除以零。 |
SIGILL | 非法指令,如執行垃圾或特權指令 |
SIGSYS | 糟糕的系統調用 |
SIGXCPU | 超過CPU時間限制。 |
SIGXFSZ | 文件大小限制。 |
一般的出現崩潰信號,Android 系統默認缺省操作是直接退出我們的程序。但是系統允許我們給某一個進程的某一個特定信號注冊一個相應的處理函數(signal),即對該信號的默認處理動作進行修改。因此 NDK Crash 的監控可以采用這種信號機制,捕獲崩潰信號執行我們自己的信號處理函數從而捕獲 NDK Crash。
墓碑
普通應用無權限讀取墓碑文件,墓碑文件位于路徑/data/tombstones/下。解析墓碑文件與后面的 breakPad 都可使用 addr2line 工具。
Android 本機程序本質上就是一個 Linux 程序,當它在執行時發生嚴重錯誤,也會導致程序崩潰,然后產生一個記錄崩潰的現場信息的文件,而這個文件在 Android 系統中就是 tombstones 墓碑文件。
BreakPad
Google breakpad 是一個跨平臺的崩潰轉儲和分析框架和工具集合,其開源地址是:https://github.com/google/breakpad。breakpad 在 Linux 中的實現就是借助了 Linux 信號捕獲機制實現的。因為其實現為 C++,因此在 Android 中使用,必須借助 NDK 工具。
引入項目
將Breakpad源碼下載解壓,首先查看 README.ANDROID 文件。
打開 README.ANDROID
按照文檔中的介紹,如果我們使用 Android.mk 非常簡單就能夠引入到我們工程中,但是目前 NDK 默認的構建工具為:CMake,因此我們做一次移植。查看android/google_breakpad/Android.mk
LOCAL_PATH := $(call my-dir)/../..
include $(CLEAR_VARS)
#最后編譯出 libbreakpad_client.a
LOCAL_MODULE := breakpad_client
#指定c++源文件后綴名
LOCAL_CPP_EXTENSION := .cc
# 強制構建系統以 32 位 arm 模式生成模塊的對象文件
LOCAL_ARM_MODE := arm
# 需要編譯的源碼
LOCAL_SRC_FILES := \
src/client/linux/crash_generation/crash_generation_client.cc \
src/client/linux/dump_writer_common/thread_info.cc \
src/client/linux/dump_writer_common/ucontext_reader.cc \
src/client/linux/handler/exception_handler.cc \
src/client/linux/handler/minidump_descriptor.cc \
src/client/linux/log/log.cc \
src/client/linux/microdump_writer/microdump_writer.cc \
src/client/linux/minidump_writer/linux_dumper.cc \
src/client/linux/minidump_writer/linux_ptrace_dumper.cc \
src/client/linux/minidump_writer/minidump_writer.cc \
src/client/minidump_file_writer.cc \
src/common/convert_UTF.cc \
src/common/md5.cc \
src/common/string_conversion.cc \
src/common/linux/breakpad_getcontext.S \
src/common/linux/elfutils.cc \
src/common/linux/file_id.cc \
src/common/linux/guid_creator.cc \
src/common/linux/linux_libc_support.cc \
src/common/linux/memory_mapped_file.cc \
src/common/linux/safe_readlink.cc
#導入頭文件
LOCAL_C_INCLUDES := $(LOCAL_PATH)/src/common/android/include \
$(LOCAL_PATH)/src \
$(LSS_PATH) #注意這個目錄
#導出頭文件
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES)
#使用android ndk中的日志庫log
LOCAL_EXPORT_LDLIBS := -llog
#編譯static靜態庫-》類似java的jar包
include $(BUILD_STATIC_LIBRARY)
注意:mk文件中 LOCAL_C_INCLUDES 的 LSS_PATH 是個坑
對照 Android.mk 文件,我們在自己項目的 cpp(工程中C/C++源碼)目錄下創建 breakpad 目錄,并將下載的 breakpad 源碼根目錄下的 src 目錄全部復制到我們的項目中:
需要在 build.gradle 中配置
接下來在 breakpad 目錄下創建 CMakeLists.txt 文件( AS 安裝 CMake Simple highlighter 插件使 CMakeLists.txt 高亮顯示):
具體可以參照NDK和配置 CMake
cmake_minimum_required(VERSION 3.4.1)
#對應android.mk中的 LOCAL_C_INCLUDES
include_directories(breakpad/src breakpad/src/common/android/include)
#開啟arm匯編支持,因為在源碼中有 .S文件(匯編源碼) enable_language(ASM)
#生成 libbreakpad.a 并指定源碼,對應android.mk中 LOCAL_SRC_FILES+LOCAL_MODULE
add_library(breakpad STATIC
src/client/linux/crash_generation/crash_generation_client.cc
src/client/linux/dump_writer_common/thread_info.cc
src/client/linux/dump_writer_common/ucontext_reader.cc
src/client/linux/handler/exception_handler.cc
src/client/linux/handler/minidump_descriptor.cc src/client/linux/log/log.cc
src/client/linux/microdump_writer/microdump_writer.cc
src/client/linux/minidump_writer/linux_dumper.cc
src/client/linux/minidump_writer/linux_ptrace_dumper.cc
src/client/linux/minidump_writer/minidump_writer.cc
src/client/minidump_file_writer.cc src/common/convert_UTF.cc
src/common/md5.cc src/common/string_conversion.cc
src/common/linux/breakpad_getcontext.S src/common/linux/elfutils.cc
src/common/linux/file_id.cc src/common/linux/guid_creator.cc
src/common/linux/linux_libc_support.cc
src/common/linux/memory_mapped_file.cc
src/common/linux/safe_readlink.cc)
#鏈接 log庫,對應android.mk中 LOCAL_EXPORT_LDLIBS
target_link_libraries(breakpad log)
在 cpp 目錄下(breakpad同級)還有一個 CMakeLists.txt 文件,它的內容是:
cmake_minimum_required(VERSION 3.4.1)
#引入breakpad的頭文件(api的定義)
include_directories(breakpad/src breakpad/src/common/android/include)
#引入breakpad的cmakelist,執行并生成libbreakpad.a (api的實現,類似java的jar包)
add_subdirectory(breakpad)
#生成libbugly.so 源碼是:bugly.cpp(我們自己的源碼,要使用breakpad)
add_library(bugly SHARED bugly.cpp)
# 鏈接ndk中的log庫
target_link_libraries(
bugly
breakpad#引入breakpad的庫文件(api的實現)
log)
此時執行編譯,會在 #include "third_party/lss/linux_syscall_support.h" 報錯,無法找到頭文件。此文件從:https://chromium.googlesource.com/external/linux-syscall-support/+/refs/heads/master 下載(需要翻墻)放到工程對應目錄即可。
bugly.cpp
源文件中的實現為:
#include <jni.h>
#include <android/log.h>
#include "breakpad/src/client/linux/handler/minidump_descriptor.h"
#include "breakpad/src/client/linux/handler/exception_handler.h"
bool DumpCallback(const google_breakpad::MinidumpDescriptor &descriptor,
void *context,
bool succeeded) {
__android_log_print(ANDROID_LOG_ERROR, "ndk_crash", "Dump path: %s", descriptor.path());
//如果回調返回true,Breakpad將把異常視為已完全處理,禁止任何其他處理程序收到異常通知。
//如果回調返回false,Breakpad會將異常視為未處理,并允許其他處理程序處理它。
return false;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_wuc_crash_CrashReport_initBreakpad(JNIEnv *env, jclass type, jstring path_) {
const char *path = env->GetStringUTFChars(path_, 0);
//開啟crash監控
google_breakpad::MinidumpDescriptor descriptor(path);
static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback, NULL, true, -1);
env->ReleaseStringUTFChars(path_, path);
}
//測試用
extern "C"
JNIEXPORT void JNICALL
Java_com_wuc_crash_CrashReport_testNativeCrash(JNIEnv *env, jclass clazz) {
int *i = NULL;
*i = 1;
}
注意 JNI 方法的方法名對應了 java 類,創建 Java 源文件: com.wuc.crash.CrashReport
package com.wuc.crash;
import android.content.Context;
import java.io.File;
public class CrashReport {
static {
System.loadLibrary("bugly");
}
public static void init(Context context) {
//開啟java監控
Context applicationContext = context.getApplicationContext();
CrashHandler.getInstance().init(applicationContext);
//開啟ndk監控
File file = new File(context.getExternalCacheDir(), "native_crash");
if (!file.exists()) {
file.mkdirs();
}
initBreakpad(file.getAbsolutePath());
}
// C++: Java_com_enjoy_crash_CrashReport_initBreakpad
private static native void initBreakpad(String path);
// C++: Java_com_enjoy_crash_CrashReport_testNativeCrash
public static native void testNativeCrash();
public static void testJavaCrash() {
int i = 1 / 0;
}
}
此時,如果出現 NDK Crash,會在我們指定的目
錄: /sdcard/Android/Data/[packageName]/cache/native_crash
下生成 NDK Crash 信息文件。
Crash 解析
采集到的 Crash 信息記錄在 minidump 文件中。minidump 是由微軟開發的用于崩潰上傳的文件格式。我們可以將此文件上傳到服務器完成上報,但是此文件沒有可讀性可言,要將文件解析為可讀的崩潰堆棧需要按照 breakpad 文檔編譯minidump_stackwalk 工具。不過好在,無論你是 Mac、windows 還是 ubuntu 在 Android Studio 的安裝目錄下的 bin\lldb\bin 面就存在一個對應平臺的 minidump_stackwalk 。
使用這里的工具執行:
minidump_stackwalk xxxx.dump > crash.txt
打開 crash.txt 內容為:
Operating system: Android
0.0.0 Linux 5.4.61-android11-0-00791-gbad091cc4bf3-ab6833933 #1 SMP PREEMPT 2020-09-14 14:42:20 i686
CPU: x86 // abi類型
GenuineIntel family 6 model 142 stepping 10
4 CPUs
Crash reason: SIGSEGV //內存引用無效 信號
Crash address: 0x0
Process uptime: not available
Thread 0 (crashed) //crashed:出現crash的線程
0 libbugly.so + 0x1fee4 //crash的so與寄存器信息
eip = 0xdfea8ee4 esp = 0xff81d6c0 ebp = 0xff81d6f8 ebx = 0xdff23460
esi = 0xdff037f8 edi = 0xdff037ff eax = 0x00000001 ecx = 0x00000000
edx = 0x00000001 efl = 0x00010246
Found by: given as instruction pointer in context
1 libart.so + 0x142133
eip = 0xe3292133 esp = 0xff81d700 ebp = 0xff81d720
Found by: previous frame's frame pointer
Thread 1
...
接下來使用 Android NDK 里面提供的 addr2line 工具將寄存器地址轉換為對應符號。addr2line 要用和自己 so 的 ABI 匹配的目錄,同時需要使用有符號信息的so(一般debug的就有)。
因為我使用的是模擬器x86架構,因此 addr2line 位于:
android/android-sdk-macosx/ndk/21.1.6352462/toolchains/x86-4.9/prebuilt/darwin-x86_64/bin/i686-linux-android-addr2line
i686-linux-android-addr2line -f -C -e libbugly.so 0x1fee4