Crash監(jiān)控-Java&NDK

Crash(應(yīng)用崩潰)是由于代碼異常而導(dǎo)致 App 非正常退出说庭,導(dǎo)致應(yīng)用程序無法繼續(xù)使用税手,所有工作都 停止的現(xiàn)象。發(fā)生 Crash 后需要重新啟動應(yīng)用(有些情況會自動重啟)檐束,而且不管應(yīng)用在開發(fā)階段做得 多么優(yōu)秀,也無法避免 Crash 發(fā)生般码,特別是在 Android 系統(tǒng)中妻率,系統(tǒng)碎片化嚴(yán)重、各 ROM 之間的差 異侈询,甚至系統(tǒng)Bug舌涨,都可能會導(dǎo)致Crash的發(fā)生糯耍。
在 Android 應(yīng)用中發(fā)生的 Crash 有兩種類型扔字,Java 層的 Crash 和 Native 層 Crash。這兩種Crash 的監(jiān) 控和獲取堆棧信息有所不同温技。

Java Crash

Java的Crash監(jiān)控非常簡單革为,Java中的Thread定義了一個接口: ;用于 處理未捕獲的異常導(dǎo)致線程的終止(注意:catch了的是捕獲不到的),當(dāng)我們的應(yīng)用crash的時候舵鳞,就 會走 UncaughtExceptionHandler.uncaughtException 震檩,在該方法中可以獲取到異常的信息,我們通 過 Thread.setDefaultUncaughtExceptionHandler 該方法來設(shè)置線程的默認(rèn)異常處理器蜓堕,我們可以 將異常信息保存到本地或者是上傳到服務(wù)器抛虏,方便我們快速的定位問題。

public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private static final String FILE_NAME_SUFFIX = ".trace";
    private static Thread.UncaughtExceptionHandler mDefaultCrashHandler;
    private static Context mContext;

    private CrashHandler() {
    }

    public static void init(@NonNull Context context) { //默認(rèn)為:RuntimeInit#KillApplicationHandler
        mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
        mContext = context.getApplicationContext();
    }

    /**
     * 當(dāng)程序中有未被捕獲的異常套才,系統(tǒng)將會調(diào)用這個方法 *
     *
     * @param t 出現(xiàn)未捕獲異常的線程
     * @param e 得到異常信息
     */
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        try {
//自行處理:保存本地
            File file = dealException(e); //上傳服務(wù)器
//......
        } catch (Exception e1) {
            e1.printStackTrace();
        } finally { //交給系統(tǒng)默認(rèn)程序處理
            if (mDefaultCrashHandler != null) {
                mDefaultCrashHandler.uncaughtException(t, e);
            }
        }
    }

    /**
     * 導(dǎo)出異常信息到SD卡 *
     *
     * @param e
     */
    private File dealException(Thread t, Throwable e) throws Exception {
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new
                Date());
        File f = new
                File(context.getExternalCacheDir().getAbsoluteFile(), "crash_info");
        if (!f.exists()) {
            f.mkdirs();
        }
        File crashFile = new File(f, time + FILE_NAME_SUFFIX);
        File file = new File(PATH + File.separator + time + FILE_NAME_SUFFIX);
//往文件中寫入數(shù)據(jù)
        PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));
        pw.println(time);
        pw.println("Thread: " + t.getName());
        try {
            pw.println(getPhoneInfo());
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        e.printStackTrace(pw); //寫入crash堆棧 
        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 + "\n");
//Android版本號
        sb.append("OS Version: ");
        sb.append(Build.VERSION.RELEASE);
        sb.append("_");
        sb.append(Build.VERSION.SDK_INT + "\n");
//手機(jī)制造商
        sb.append("Vendor: ");
        sb.append(Build.MANUFACTURER + "\n");
//手機(jī)型號
        sb.append("Model: ");
        sb.append(Build.MODEL + "\n");

        //CPU架構(gòu)
        sb.append("CPU: ");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            sb.append(Arrays.toString(Build.SUPPORTED_ABIS));
        } else {
            sb.append(Build.CPU_ABI);
        }
        return sb.toString();
    }
}

NDK Crash

相對于Java的Crash迂猴,NDK的錯誤無疑更加讓人頭疼,特別是對初學(xué)NDK的同學(xué)背伴,不說監(jiān)控沸毁,就算是錯 誤堆棧都不知道怎么看。

Linux信號機(jī)制

信號機(jī)制是Linux進(jìn)程間通信的一種重要方式傻寂,Linux信號一方面用于正常的進(jìn)程間通信和同步息尺,另一方 面它還負(fù)責(zé)監(jiān)控系統(tǒng)異常及中斷。當(dāng)應(yīng)用程序運(yùn)行異常時疾掰,Linux內(nèi)核將產(chǎn)生錯誤信號并通知當(dāng)前進(jìn) 程搂誉。當(dāng)前進(jìn)程在接收到該錯誤信號后,可以有三種不同的處理方式静檬。

  • 忽略該信號;
  • 捕捉該信號并執(zhí)行對應(yīng)的信號處理函數(shù)(信號處理程序);
  • 執(zhí)行該信號的缺省操作(如終止進(jìn)程)

當(dāng)Linux應(yīng)用程序在執(zhí)行時發(fā)生嚴(yán)重錯誤勒葱,一般會導(dǎo)致程序崩潰。其中巴柿,Linux專門提供了一類crash信 號凛虽,在程序接收到此類信號時,缺省操作是將崩潰的現(xiàn)場信息記錄到核心文件广恢,然后終止進(jìn)程凯旋。

常見崩潰信號列表:

信號 描述
SIGSEGV 內(nèi)存引用無效。
SIGBUS 訪問內(nèi)存對象的未定義部分。
SIGFPE 算術(shù)運(yùn)算錯誤至非,除以零钠署。
SIGILL 非法指令,如執(zhí)行垃圾或特權(quán)指令
SIGSYS 糟糕的系統(tǒng)調(diào)用
SIGXCPU 超過CPU時間限制荒椭。
SIGXFSZ 文件大小限制谐鼎。

一般的出現(xiàn)崩潰信號,Android系統(tǒng)默認(rèn)缺省操作是直接退出我們的程序趣惠。但是系統(tǒng)允許我們給某一個 進(jìn)程的某一個特定信號注冊一個相應(yīng)的處理函數(shù)(signal)狸棍,即對該信號的默認(rèn)處理動作進(jìn)行修改。因 此NDK Crash的監(jiān)控可以采用這種信號機(jī)制味悄,捕獲崩潰信號執(zhí)行我們自己的信號處理函數(shù)從而捕獲NDK Crash草戈。

墓碑

此處了解即可,普通應(yīng)用無權(quán)限讀取墓碑文件侍瑟,墓碑文件位于路徑/data/tombstones/下唐片。解析墓 碑文件與后面的breakPad都可使用 addr2line 工具。

Android本機(jī)程序本質(zhì)上就是一個Linux程序涨颜,當(dāng)它在執(zhí)行時發(fā)生嚴(yán)重錯誤费韭,也會導(dǎo)致程序崩潰,然后產(chǎn) 生一個記錄崩潰的現(xiàn)場信息的文件庭瑰,而這個文件在Android系統(tǒng)中就是 tombstones 墓碑文件星持。

BreakPad

Google breakpad是一個跨平臺的崩潰轉(zhuǎn)儲和分析框架和工具集合,其開源地址是:https://github.co m/google/breakpad见擦。breakpad在Linux中的實現(xiàn)就是借助了Linux信號捕獲機(jī)制實現(xiàn)的钉汗。因為其實現(xiàn) 為C++,因此在Android中使用鲤屡,必須借助NDK工具损痰。

引入項目

將Breakpad源碼下載解壓,首先查看README.ANDROID文件酒来。

打開 README.ANDROID

crash1.png

按照文檔中的介紹卢未,如果我們使用Android.mk 非常簡單就能夠引入到我們工程中,但是目前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 模式生成模塊的對象文件 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) #注意這個目錄
#導(dǎo)出頭文件
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES) 
#使用android ndk中的日志庫log 
LOCAL_EXPORT_LDLIBS := -llog
#編譯static靜態(tài)庫-》類似java的jar包 
include $(BUILD_STATIC_LIBRARY)

注意:mk文件中 LOCAL_C_INCLUDES 的 LSS_PATH

對照Android.mk文件,我們在自己項目的cpp(工程中C/C++源碼)目錄下創(chuàng)建breakpad目錄翘鸭,并將下載
的breakpad源碼根目錄下的src目錄全部復(fù)制到我們的項目中:

crash2.png

接下來在breakpad目錄下創(chuàng)建CMakeList.txt文件:

cmake_minimum_required(VERSION 3.4.1) 
#對應(yīng)android.mk中的 LOCAL_C_INCLUDES include_directories(src src/common/android/include) 
#開啟arm匯編支持滴铅,因為在源碼中有 .S文件(匯編源碼) enable_language(ASM)
#生成 libbreakpad.a 并指定源碼,對應(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庫就乓,對應(yīng)android.mk中 LOCAL_EXPORT_LDLIBS target_link_libraries(breakpad log)

在cpp目錄下(breakpad同級)還有一個CMakeList.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的實現(xiàn),類似java的jar包) add_subdirectory(breakpad)
#生成libbugly.so 源碼是:ndk_crash.cpp(我們自己的源碼噩翠,要使用breakpad) 
add_library(
bugly
SHARED ndk_crash.cpp)
target_link_libraries(
        bugly
breakpad #引入breakpad的庫文件(api的實現(xiàn)) 
log)

此時執(zhí)行編譯戏自,會在 #include "third_party/lss/linux_syscall_support.h" 報錯,無法找到頭 文件伤锚。此文件從:https://chromium.googlesource.com/external/linux-syscall-support/+/refs/head s/master 下載放到工程對應(yīng)目錄即可擅笔。

ndk_crash.cpp 源文件中的實現(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會將異常視為未處理玄呛,并允許其他處理程序處理它阅懦。
return false;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_enjoy_crash_CrashReport_initBreakpad(JNIEnv *env, jclass type, jstring
path_) {
const char *path = env->GetStringUTFChars(path_, 0);
//開啟crash監(jiān)控
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_enjoy_crash_CrashReport_testNativeCrash(JNIEnv *env, jclass clazz) {
int *i = NULL;
*i = 1; }

注意JNI方法的方法名對應(yīng)了java類和二,創(chuàng)建Java源文件: com.enjoy.crash.CrashReport


import android.content.Context; import java.io.File;
public class CrashReport {
static { System.loadLibrary("bugly");
}
public static void init(Context context) { //開啟java監(jiān)控
Context applicationContext = context.getApplicationContext(); CrashHandler.init(applicationContext);
//開啟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 int testJavaCrash() {
        return 1 / 0;
} }

此時徘铝,如果出現(xiàn)NDK Crash,會在我們指定的目
錄: /sdcard/Android/Data/[packageName]/cache/native_crash 下生成NDK Crash信息文件惯吕。

Crash解析

采集到的Crash信息記錄在minidump文件中惕它。minidump是由微軟開發(fā)的用于崩潰上傳的文件格式。我 們可以將此文件上傳到服務(wù)器完成上報废登,但是此文件沒有可讀性可言淹魄,要將文件解析為可讀的崩潰堆棧 需要按照breakpad文檔編譯 工具,而Windows系統(tǒng)編譯個人不會堡距。不過好在甲锡, 無論你是 Mac、windows還是ubuntu在 Android Studio 的安裝目錄下的 bin\lldb\bin 里面就存在一 個對應(yīng)平臺的 羽戒。

crash3.png

使用這里的工具執(zhí)行:

minidump_stackwalk xxxx.dump > crash.txt

打開crash.txt 內(nèi)容為:

Operating system: Android
0.0.0 Linux 4.4.124+ #1 SMP PREEMPT Wed Jan 30 07:13:09 UTC
2019 i686
CPU: x86 // abi類型
     GenuineIntel family 6 model 31 stepping 1
     3 CPUs
GPU: UNKNOWN
Crash reason: SIGSEGV //內(nèi)存引用無效 信號 Crash address: 0x0
Process uptime: not available
Thread 0 (crashed) //crashed:出現(xiàn)crash的線程
0 libbugly.so + 0x1feab //crash的so與寄存器信息
eip = 0xd5929eab
esi = 0xd71a3f04
edx = 0x00000000
Found by: given as instruction pointer in context
ebx = 0x0000000c
ecx = 0xefb19400
esp = 0xffa85f30   ebp = 0xffa85f38
edi = 0xffa86128   eax = 0xffa85f5c
efl = 0x00210286
1 libart.so + 0x5f6a18
eip = 0xef92ea18 esp = 0xffa85f40 ebp = 0xffa85f60 Found by: previous frame's frame pointer
Thread 1 ......

接下來使用 Android NDK 里面提供的 addr2line 工具將寄存器地址轉(zhuǎn)換為對應(yīng)符號缤沦。addr2line 要用和 自己 so 的 ABI 匹配的目錄,同時需要使用有符號信息的so(一般debug的就有)易稠。

因為我使用的是模擬器x86架構(gòu)缸废,因此addr2line位于:
Android\Sdk\ndk\21.3.6528147\toolchains\x86-4.9\prebuilt\windows-x86_64\bin\i686-linux- android-addr2line.exe

i686-linux-android-addr2line.exe -f -C -e libbugly.so 0x1feab
crash4.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者驶社。
  • 序言:七十年代末企量,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子亡电,更是在濱河造成了極大的恐慌届巩,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件份乒,死亡現(xiàn)場離奇詭異恕汇,居然都是意外死亡零酪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進(jìn)店門拇勃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來四苇,“玉大人,你說我怎么就攤上這事方咆≡乱福” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵瓣赂,是天一觀的道長榆骚。 經(jīng)常有香客問我,道長煌集,這世上最難降的妖魔是什么妓肢? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮苫纤,結(jié)果婚禮上碉钠,老公的妹妹穿的比我還像新娘。我一直安慰自己卷拘,他們只是感情好喊废,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著栗弟,像睡著了一般污筷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上乍赫,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天瓣蛀,我揣著相機(jī)與錄音,去河邊找鬼雷厂。 笑死惋增,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的罗侯。 我是一名探鬼主播器腋,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼钩杰!你這毒婦竟也來了纫塌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤讲弄,失蹤者是張志新(化名)和其女友劉穎措左,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體避除,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怎披,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年胸嘁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凉逛。...
    茶點(diǎn)故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡性宏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出状飞,到底是詐尸還是另有隱情毫胜,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布诬辈,位于F島的核電站酵使,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏焙糟。R本人自食惡果不足惜口渔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望穿撮。 院中可真熱鬧缺脉,春花似錦、人聲如沸混巧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咧党。三九已至,卻和暖如春陨亡,著一層夾襖步出監(jiān)牢的瞬間傍衡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工负蠕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蛙埂,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓遮糖,卻偏偏與公主長得像绣的,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子欲账,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評論 2 361