Android端crash可分為Java層crash和Native crash,我們通常說的crash一般指的是Java層crash,Native crash主要指C/C++代碼(其在Android工程中以動(dòng)態(tài)鏈接庫的形式存在)的崩潰,一般難以抓取。下圖為Android系統(tǒng)框架圖,NativeCrash主要就是在圖中紅框部分發(fā)生的崩潰。
Android中C/C++開發(fā)部分稱之為NDK,Android開發(fā)中引入NDK一般是基于如下考慮:
數(shù)據(jù)安全:java層代碼易被反編譯,而C/C++庫反匯難度較大。
性能考慮:將要求高性能的應(yīng)用邏輯使用C開發(fā),提高應(yīng)用程序的執(zhí)行效率。
便于移植。用C/C++寫得庫可以方便在其他的嵌入式平臺(tái)上再次使用
對于NativeCrash,其復(fù)現(xiàn)難度大,且需要手機(jī)root權(quán)限(Native崩潰日志存儲(chǔ)在手機(jī)的/data/tombstones目錄下)。本文主要介紹兩種方法獲取Android端線上Native Crash的崩潰信息,分別是基于開源工具google-breakpad和基于c/c++信號異常處理。
首先介紹在開源工具google-breakpad的基礎(chǔ)上,實(shí)時(shí)抓取線上Native crash的dmp日志,并對dmp日志進(jìn)行上傳、解析、分類、聚合的過程。下圖為google-breakpad的工作原理圖。Breakpad有三個(gè)主要組件:
? 客戶端是一個(gè)庫,包含在您的應(yīng)用程序中。 它可以獲取當(dāng)前線程的狀態(tài)和當(dāng)前加載的可執(zhí)行文件和共享庫的ID寫轉(zhuǎn)儲(chǔ)文件。您可以配置客戶端發(fā)生了崩潰時(shí)寫入一個(gè)minidump時(shí),或明確要求時(shí)。
? ?符號卸載器是一個(gè)程序,讀取由編譯器產(chǎn)生的調(diào)試信息,并生成一個(gè)使用Breakpad格式的符號文件? 。
? 該處理器(minidump processor)是一個(gè)程序,讀取一個(gè)minidump文件,找到相應(yīng)的版本的符號文件的(可執(zhí)行文件和共享庫的轉(zhuǎn)儲(chǔ)提到的),并產(chǎn)生了一個(gè)人可讀的C / C + +堆棧跟蹤。
關(guān)于google-breakpad的工作原理可詳見如下鏈接
http://blog.csdn.net/wpc320/article/details/8290501
實(shí)現(xiàn)方案
上圖給出NativeCrash整個(gè)實(shí)現(xiàn)方案,APP中接入提供的SDK,包含一個(gè)通用SO和一個(gè)JAR,當(dāng)APP中發(fā)生NDK崩潰時(shí),會(huì)在手機(jī)端生成一個(gè)dmp文件,待下次APP重啟后,將此dmp文件上傳至服務(wù)端,在服務(wù)端進(jìn)行解析、分類、聚合、可讀展示等過程。總的來說,可分為三個(gè)模塊
?? 客戶端sdk
客戶端sdk主要包含一個(gè)通用so和jar,通用so主要是基于google-breakpad進(jìn)行簡單封裝,使其通用化,jar主要提供上傳dmp文件的功能。通用so采用NDK編程,主要代碼片段如下:
JNI_OnLoad
```
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
goto bail;
}
if (register_nativecrash(env) < 0) {
goto bail;
}
fields.jvm = vm;
result = JNI_VERSION_1_4;
bail: return result;
}
```
register_nativecrash:注冊native方法
static int register_nativecrash(JNIEnv *env) {
return registerNativeMethods(env, CLASSPATH, gMethods,
sizeof(gMethods) / sizeof(gMethods[0]));
}
static int registerNativeMethods(JNIEnv* env, const char* className,
JNINativeMethod* gMethods, int numMethods) {
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
fields.clazz = (jclass) env->NewGlobalRef(clazz);
if (fields.clazz == NULL) {
return JNI_FALSE;
}
return JNI_TRUE;
}
設(shè)置dmp文件存放目錄
static void Java_com_baidu_nativecrash_NativeCrash_setNativeCrashDir(
JNIEnv* env, jobject thiz, jstring dir) {
if (dir == NULL) {
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
"native crash dir is null");
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}
const char *filepathStr = env->GetStringUTFChars(dir, NULL);
if (filepathStr == NULL) {? // Out of memory
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
"native crash filepathStr is null");
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}
if (filepathStr && (strlen(filepathStr) > 0)) {
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "native crash %s",
filepathStr);
static google_breakpad::MinidumpDescriptor descriptor(filepathStr);
static google_breakpad::ExceptionHandler eh(descriptor, NULL,
DumpCallback, NULL, true, -1);
}
DumpCallback回調(diào)方法中可以向Jave層回傳一些其他額外信息。
}
android.mk
MY_ROOT_PATH := $(call my-dir)
include $(MY_ROOT_PATH)/../google-breakpad/android/jni/Android.mk
LOCAL_PATH := $(MY_ROOT_PATH)
include $(CLEAR_VARS)
LOCAL_MODULE? ? := nativecrash
LOCAL_SRC_FILES := nativecrash_jni.cpp
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/../google-breakpad/src/common/android/include \
$(LOCAL_PATH)/../google-breakpad/src
LOCAL_CPPFLAGS :=? -Os -fvisibility=hidden
LOCAL_CFLAGS :=? -Os -fvisibility=hidden
LOCAL_CFLAGS += -Wno-psabi
LOCAL_STATIC_LIBRARIES += breakpad_client
include $(BUILD_SHARED_LIBRARY)
Application.mk
APP_ABI := armeabi-v7a armeabi mips x86
APP_STL := stlport_static
APP_OPTIM := release
APP_CPPFLAGS += -Wno-error=format-security
? native日志上傳server
客戶端dmp文件上傳的服務(wù)端采用百度云服務(wù),主要用來接收客戶端上傳的dmp文件并保存。注意,這里dmp文件上傳時(shí)機(jī)為APP重啟后,在wifi的情況下上傳(dmp文件較大,非wifi上傳不合理)。接收客戶端上傳的dmp文件并存儲(chǔ)后寫庫,數(shù)據(jù)庫中需要兩個(gè)表,分別是用來保存dmp文件的存儲(chǔ)記錄表以及dmp文件解析后的日志分類表。
? 日志解析分類server
—解析過程:每隔3min讀取日志存儲(chǔ)記錄表最近30條未解析記錄,進(jìn)行解析,dmp解析需要用到breakpad的minidump processor提供的解析方法,故需事先編譯breakpad源碼,生成解析所需tool,解析完成后,修改解析狀態(tài)。解析腳本需要輪詢執(zhí)行,通過linux下crontab進(jìn)行設(shè)置。解析過程參考如下鏈接。
http://blog.csdn.net/brook0344/article/details/20126351
—分類過程:每隔3min讀取存儲(chǔ)記錄表最近20條已解析記錄,進(jìn)行分類。分類算法主要是將解析后的崩潰堆棧信息的棧頂文本作為關(guān)鍵字,進(jìn)行分類,可大大簡化文本分類過程。我們可以認(rèn)為在同一處崩潰的堆棧信息是一致的,也即可以認(rèn)為棧頂文本相同的崩潰,是同一類崩潰。分類完成后,寫入日志分類表。
—日志展示:讀取日志分類表,按分類的崩潰次數(shù)逆序排序,展示結(jié)果。
以上是基于google-breakpad的NativeCrash日志收集方法的全過程,google-breakpad是夸平臺(tái)開源工具,體量較大,在其基礎(chǔ)上生成的通用SO(用到STL)和dmp日志也都較大,對于sdk大小有嚴(yán)格要求的APP,可能不是很方便。下一節(jié)介紹體量較小的基于c/c++異常信號處理的NativeCrash日志收集方法。