Crash 監控

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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • Crash(應用崩潰)是由于代碼異常而導致 App 非正常退出,導致應用程序無法繼續使用,所有工作都 停止的現象。...
    zcwfeng閱讀 708評論 0 2
  • 原文[http://www.lxweimin.com/p/4278915847b6]源碼[https://down...
    WaterYuan閱讀 2,122評論 1 2
  • Android異常監控 Crash就是由于代碼異常而導致App非正常退出現象,也就是我們常說的『崩潰』 通常情況下...
    Heezier閱讀 1,127評論 2 6
  • 背景: 支付SDK面向游戲提供支付服務,高效的游戲引擎一般都會C++編寫的,通過NDK編譯成so文件在Androi...
    jpchen_hn閱讀 4,973評論 0 6
  • 1. 找到未strip的, 符號表完整的so庫文件 在Android Studio 3.2.1: strip之前的...
    hjm1fb閱讀 4,336評論 3 3