Crash(應(yīng)用崩潰)是由于代碼異常而導(dǎo)致 App 非正常退出僻爽,導(dǎo)致應(yīng)用程序無(wú)法繼續(xù)使用顿锰,所有工作都停止的現(xiàn)象蛔糯。發(fā)生 Crash 后需要重新啟動(dòng)應(yīng)用(有些情況會(huì)自動(dòng)重啟)陈瘦。
在 Android 應(yīng)用中發(fā)生的 Crash 有兩種類型幌甘,Java 層的 Crash 和 Native 層 Crash。這兩種Crash 的監(jiān)控和獲取堆棧信息有所不同痊项。
Java Crash
Java 的 Crash 監(jiān)控比較簡(jiǎn)單锅风,Java 中的 Thread 定義了一個(gè)接口UncaughtExceptionHandler
;用于處理未捕獲的異常導(dǎo)致線程的終止(注意:catch了的是捕獲不到的)鞍泉,當(dāng)我們的應(yīng)用 crash 的時(shí)候皱埠,就會(huì)走 UncaughtExceptionHandler.uncaughtException
,在該方法中可以獲取到異常的信息咖驮,我們通過(guò) Thread.setDefaultUncaughtExceptionHandler
該方法來(lái)設(shè)置線程的默認(rèn)異常處理器边器,我們可以將異常信息保存到本地或者是上傳到服務(wù)器训枢,方便我們快速的定位問(wèn)題。
/**
* UncaughtException處理類, 當(dāng)程序發(fā)生Uncaught異常的時(shí)候, 有該類來(lái)接管程序, 并記錄發(fā)送錯(cuò)誤報(bào)告
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String FILE_NAME_SUFFIX = ".trace";
/**
* 系統(tǒng)默認(rèn)的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) {
// 獲取系統(tǒng)默認(rèn)的UncaughtException處理器
//默認(rèn)為:RuntimeInit#KillApplicationHandler
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
//設(shè)置該CrashHandler為程序的默認(rèn)處理器
Thread.setDefaultUncaughtExceptionHandler(this);
mContext = context.getApplicationContext();
}
/***
* 當(dāng)程序中有未被捕獲的異常忘巧,系統(tǒng)將會(huì)調(diào)用這個(gè)方法
* @param thread 出現(xiàn)未捕獲異常的線程
* @param throwable 得到異常信息
*/
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
try {
//自行處理:保存本地
File file = dealException(thread, throwable);
//上傳服務(wù)器
//......
} catch (Exception e) {
e.printStackTrace();
} finally {
//交給系統(tǒng)默認(rèn)程序處理
if (mDefaultCrashHandler != null) {
// 如果用戶沒(méi)有處理則讓系統(tǒng)默認(rèn)的異常處理器來(lái)處理
mDefaultCrashHandler.uncaughtException(thread, throwable);
}
}
}
/**
* 導(dǎo)出異常信息到SD卡
*/
private File dealException(Thread thread, Throwable e) throws Exception {
//存儲(chǔ)位置: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());
// //往文件中寫入數(shù)據(jù)
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();
// 迭代棧隊(duì)列把所有的異常信息寫入writer中
while (mThrowable != null) {
mThrowable.printStackTrace(pw);
// 換行 每個(gè)個(gè)異常棧之間換行
pw.append("\r\n");
mThrowable = mThrowable.getCause();
}
pw.close();
return file;
}
/**
* 記錄手機(jī)信息
*/
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版本號(hào)
sb.append("OS Version: ");
sb.append(Build.VERSION.RELEASE);
sb.append("_");
sb.append(Build.VERSION.SDK_INT).append("\n");
//手機(jī)制造商
sb.append("Vendor: ");
sb.append(Build.MANUFACTURER).append("\n");
//手機(jī)型號(hào)
sb.append("Model: ");
sb.append(Build.MODEL).append("\n");
//CPU架構(gòu)
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
相對(duì)于 Java 的 Crash恒界,NDK 的錯(cuò)誤無(wú)疑更加讓人頭疼。
Linux信號(hào)機(jī)制
信號(hào)機(jī)制是 Linux 進(jìn)程間通信的一種重要方式砚嘴,Linux 信號(hào)一方面用于正常的進(jìn)程間通信和同步十酣,另一方面它還負(fù)責(zé)監(jiān)控系統(tǒng)異常及中斷。當(dāng)應(yīng)用程序運(yùn)行異常時(shí)际长,Linux內(nèi)核將產(chǎn)生錯(cuò)誤信號(hào)并通知當(dāng)前進(jìn)程耸采。當(dāng)前進(jìn)程在接收到該錯(cuò)誤信號(hào)后,可以有三種不同的處理方式也颤。
- 忽略該信號(hào)洋幻;
- 捕捉該信號(hào)并執(zhí)行對(duì)應(yīng)的信號(hào)處理函數(shù)(信號(hào)處理程序);
- 執(zhí)行該信號(hào)的缺省操作(如終止進(jìn)程)翅娶;
當(dāng) Linux 應(yīng)用程序在執(zhí)行時(shí)發(fā)生嚴(yán)重錯(cuò)誤文留,一般會(huì)導(dǎo)致程序崩潰。其中竭沫,Linux 專門提供了一類 crash 信號(hào)燥翅,在程序接收到此類信號(hào)時(shí),缺省操作是將崩潰的現(xiàn)場(chǎng)信息記錄到核心文件蜕提,然后終止進(jìn)程森书。
常見(jiàn)崩潰信號(hào)列表:
信號(hào) | 描述 |
---|---|
SIGSEGV | 內(nèi)存引用無(wú)效。 |
SIGBUS | 訪問(wèn)內(nèi)存對(duì)象的未定義部分谎势。 |
SIGFPE | 算術(shù)運(yùn)算錯(cuò)誤凛膏,除以零。 |
SIGILL | 非法指令脏榆,如執(zhí)行垃圾或特權(quán)指令 |
SIGSYS | 糟糕的系統(tǒng)調(diào)用 |
SIGXCPU | 超過(guò)CPU時(shí)間限制猖毫。 |
SIGXFSZ | 文件大小限制。 |
一般的出現(xiàn)崩潰信號(hào)须喂,Android 系統(tǒng)默認(rèn)缺省操作是直接退出我們的程序吁断。但是系統(tǒng)允許我們給某一個(gè)進(jìn)程的某一個(gè)特定信號(hào)注冊(cè)一個(gè)相應(yīng)的處理函數(shù)(signal),即對(duì)該信號(hào)的默認(rèn)處理動(dòng)作進(jìn)行修改坞生。因此 NDK Crash 的監(jiān)控可以采用這種信號(hào)機(jī)制仔役,捕獲崩潰信號(hào)執(zhí)行我們自己的信號(hào)處理函數(shù)從而捕獲 NDK Crash。
墓碑
普通應(yīng)用無(wú)權(quán)限讀取墓碑文件是己,墓碑文件位于路徑/data/tombstones/下又兵。解析墓碑文件與后面的 breakPad 都可使用 addr2line 工具。
Android 本機(jī)程序本質(zhì)上就是一個(gè) Linux 程序卒废,當(dāng)它在執(zhí)行時(shí)發(fā)生嚴(yán)重錯(cuò)誤寒波,也會(huì)導(dǎo)致程序崩潰乘盼,然后產(chǎn)生一個(gè)記錄崩潰的現(xiàn)場(chǎng)信息的文件,而這個(gè)文件在 Android 系統(tǒng)中就是 tombstones 墓碑文件。
BreakPad
Google breakpad 是一個(gè)跨平臺(tái)的崩潰轉(zhuǎn)儲(chǔ)和分析框架和工具集合慨丐,其開(kāi)源地址是:https://github.com/google/breakpad叮趴。breakpad 在 Linux 中的實(shí)現(xiàn)就是借助了 Linux 信號(hào)捕獲機(jī)制實(shí)現(xiàn)的。因?yàn)槠鋵?shí)現(xiàn)為 C++希停,因此在 Android 中使用,必須借助 NDK 工具。
引入項(xiàng)目
將Breakpad源碼下載解壓辰企,首先查看 README.ANDROID 文件。
打開(kāi) README.ANDROID
按照文檔中的介紹况鸣,如果我們使用 Android.mk 非常簡(jiǎn)單就能夠引入到我們工程中牢贸,但是目前 NDK 默認(rèn)的構(gòu)建工具為: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
# 強(qiáng)制構(gòu)建系統(tǒng)以 32 位 arm 模式生成模塊的對(duì)象文件
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
#導(dǎo)入頭文件
LOCAL_C_INCLUDES := $(LOCAL_PATH)/src/common/android/include \
$(LOCAL_PATH)/src \
$(LSS_PATH) #注意這個(gè)目錄
#導(dǎo)出頭文件
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES)
#使用android ndk中的日志庫(kù)log
LOCAL_EXPORT_LDLIBS := -llog
#編譯static靜態(tài)庫(kù)-》類似java的jar包
include $(BUILD_STATIC_LIBRARY)
注意:mk文件中 LOCAL_C_INCLUDES 的 LSS_PATH 是個(gè)坑
對(duì)照 Android.mk 文件潜索,我們?cè)谧约喉?xiàng)目的 cpp(工程中C/C++源碼)目錄下創(chuàng)建 breakpad 目錄,并將下載的 breakpad 源碼根目錄下的 src 目錄全部復(fù)制到我們的項(xiàng)目中:
需要在 build.gradle 中配置
接下來(lái)在 breakpad 目錄下創(chuàng)建 CMakeLists.txt 文件( AS 安裝 CMake Simple highlighter 插件使 CMakeLists.txt 高亮顯示):
具體可以參照NDK和配置 CMake
cmake_minimum_required(VERSION 3.4.1)
#對(duì)應(yīng)android.mk中的 LOCAL_C_INCLUDES
include_directories(breakpad/src breakpad/src/common/android/include)
#開(kāi)啟arm匯編支持懂酱,因?yàn)樵谠创a中有 .S文件(匯編源碼) enable_language(ASM)
#生成 libbreakpad.a 并指定源碼竹习,對(duì)應(yīng)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庫(kù),對(duì)應(yīng)android.mk中 LOCAL_EXPORT_LDLIBS
target_link_libraries(breakpad log)
在 cpp 目錄下(breakpad同級(jí))還有一個(gè) CMakeLists.txt 文件列牺,它的內(nèi)容是:
cmake_minimum_required(VERSION 3.4.1)
#引入breakpad的頭文件(api的定義)
include_directories(breakpad/src breakpad/src/common/android/include)
#引入breakpad的cmakelist整陌,執(zhí)行并生成libbreakpad.a (api的實(shí)現(xiàn),類似java的jar包)
add_subdirectory(breakpad)
#生成libbugly.so 源碼是:bugly.cpp(我們自己的源碼瞎领,要使用breakpad)
add_library(bugly SHARED bugly.cpp)
# 鏈接ndk中的log庫(kù)
target_link_libraries(
bugly
breakpad#引入breakpad的庫(kù)文件(api的實(shí)現(xiàn))
log)
此時(shí)執(zhí)行編譯泌辫,會(huì)在 #include "third_party/lss/linux_syscall_support.h" 報(bào)錯(cuò),無(wú)法找到頭文件九默。此文件從:https://chromium.googlesource.com/external/linux-syscall-support/+/refs/heads/master 下載(需要翻墻)放到工程對(duì)應(yīng)目錄即可震放。
bugly.cpp
源文件中的實(shí)現(xiàn)為:
#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());
//如果回調(diào)返回true,Breakpad將把異常視為已完全處理荤西,禁止任何其他處理程序收到異常通知澜搅。
//如果回調(diào)返回false,Breakpad會(huì)將異常視為未處理邪锌,并允許其他處理程序處理它勉躺。
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);
//開(kāi)啟crash監(jiān)控
google_breakpad::MinidumpDescriptor descriptor(path);
static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback, NULL, true, -1);
env->ReleaseStringUTFChars(path_, path);
}
//測(cè)試用
extern "C"
JNIEXPORT void JNICALL
Java_com_wuc_crash_CrashReport_testNativeCrash(JNIEnv *env, jclass clazz) {
int *i = NULL;
*i = 1;
}
注意 JNI 方法的方法名對(duì)應(yīng)了 java 類,創(chuàng)建 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) {
//開(kāi)啟java監(jiān)控
Context applicationContext = context.getApplicationContext();
CrashHandler.getInstance().init(applicationContext);
//開(kāi)啟ndk監(jiān)控
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;
}
}
此時(shí)觅丰,如果出現(xiàn) NDK Crash饵溅,會(huì)在我們指定的目
錄: /sdcard/Android/Data/[packageName]/cache/native_crash
下生成 NDK Crash 信息文件。
Crash 解析
采集到的 Crash 信息記錄在 minidump 文件中妇萄。minidump 是由微軟開(kāi)發(fā)的用于崩潰上傳的文件格式蜕企。我們可以將此文件上傳到服務(wù)器完成上報(bào)咬荷,但是此文件沒(méi)有可讀性可言,要將文件解析為可讀的崩潰堆棧需要按照 breakpad 文檔編譯minidump_stackwalk 工具轻掩。不過(guò)好在幸乒,無(wú)論你是 Mac、windows 還是 ubuntu 在 Android Studio 的安裝目錄下的 bin\lldb\bin 面就存在一個(gè)對(duì)應(yīng)平臺(tái)的 minidump_stackwalk 唇牧。
使用這里的工具執(zhí)行:
minidump_stackwalk xxxx.dump > crash.txt
打開(kāi) crash.txt 內(nèi)容為:
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 //內(nèi)存引用無(wú)效 信號(hào)
Crash address: 0x0
Process uptime: not available
Thread 0 (crashed) //crashed:出現(xiàn)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
...
接下來(lái)使用 Android NDK 里面提供的 addr2line 工具將寄存器地址轉(zhuǎn)換為對(duì)應(yīng)符號(hào)罕扎。addr2line 要用和自己 so 的 ABI 匹配的目錄,同時(shí)需要使用有符號(hào)信息的so(一般debug的就有)丐重。
因?yàn)槲沂褂玫氖悄M器x86架構(gòu)腔召,因此 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