Android異常監(jiān)控
Crash就是由于代碼異常而導致App非正常退出現(xiàn)象霞丧,也就是我們常說的『崩潰』
通常情況下會有以下兩種類型Crash:
- Java Crash
- Native Crash
Java Crash
Java的Crash監(jiān)控非常簡單拆又,Java中的Thread定義了一個接口: UncaughtExceptionHandler 搪锣;用于
處理未捕獲的異常導致線程的終止(注意:****catch****了的是捕獲不到的)杨耙,當我們的應用crash的時候臣缀,就
會走 UncaughtExceptionHandler.uncaughtException ,在該方法中可以獲取到異常的信息匈棘,我們通
過 Thread.setDefaultUncaughtExceptionHandler 該方法來設置線程的默認異常處理器飘蚯,我們可以
將異常信息保存到本地或者是上傳到服務器熙参,方便我們快速的定位問題艳吠。
Android系統(tǒng)崩潰處理的流程:
圖片來源:https://xiang-yu.blog.csdn.net/article/details/106647428
具體的處理過程可參考:https://xiang-yu.blog.csdn.net/article/details/106647428
ZygoteInit每次fork出一個進程之后,會先初始化日志孽椰、crash監(jiān)控讲竿,再利用反射調(diào)用 ActivityThread 的main()方法啟動app。那么我們可以自己的app中重新注冊Crash監(jiān)控回調(diào)接口弄屡,然后在自己的app中處理Crash题禀。
具體的實現(xiàn)類如下:
public class CrashHandler implements Thread.UncaughtExceptionHandler{
private static final String FILE_NAME_SUFFIX = ".trace";
private static Thread.UncaughtExceptionHandler mDefaultCrashHandler;
private static Context mContext;
private static CrashHandler crashHandler;
private CrashHandler() {
}
public static CrashHandler getCrashHander(){
if (crashHandler == null){
synchronized (CrashHandler.class){
if (crashHandler == null){
crashHandler = new CrashHandler();
}
}
}
return crashHandler;
}
public void init(@NonNull Context context) {
//默認為:RuntimeInit#KillApplicationHandler
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
mContext = context.getApplicationContext();
}
/**
* 當程序中有未被捕獲的異常,系統(tǒng)將會調(diào)用這個方法
*
* @param t 出現(xiàn)未捕獲異常的線程
* @param e 得到異常信息
*/
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
//自行處理:保存本地
File file = dealException(t, e);
//上傳服務器
//......
} catch (Exception e1) {
e1.printStackTrace();
} finally {
//交給系統(tǒng)默認程序處理
if (mDefaultCrashHandler != null) {
mDefaultCrashHandler.uncaughtException(t, e);
}
}
}
/**
* 導出異常信息到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(mContext.getExternalCacheDir().getAbsoluteFile(), "crash_info");
if (!f.exists()) {
f.mkdirs();
}
File crashFile = new File(f, time + FILE_NAME_SUFFIX);
//往文件中寫入數(shù)據(jù)
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(crashFile)));
pw.println(time);
pw.println("Thread: " + t.getName());
pw.println(getPhoneInfo());
e.printStackTrace(pw); //寫入crash堆棧
pw.close();
return crashFile;
}
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");
//手機制造商sb.append("Vendor: ");
sb.append(Build.MANUFACTURER + "\n");
//手機型號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異常
NDK異常指的是app在使用c和c++ so庫的時候膀捷,如果庫運行引起app崩潰迈嘹,我們同樣可以采用類似的方式來收集和處理異常,用找出代碼問題的具體位置。
Linux信號機制了解
信號機制是Linux進程間通信的一種重要方式秀仲,Linux信號一方面用于正常的進程間通信和同步融痛,另一方
面它還負責監(jiān)控系統(tǒng)異常及中斷。當應用程序運行異常時神僵,Linux內(nèi)核將產(chǎn)生錯誤信號并通知當前進
程雁刷。當前進程在接收到該錯誤信號后,可以有三種不同的處理方式保礼。
忽略該信號沛励;
捕捉該信號并執(zhí)行對應的信號處理函數(shù)(信號處理程序);
執(zhí)行該信號的缺省操作(如終止進程)炮障;
當Linux應用程序在執(zhí)行時發(fā)生嚴重錯誤目派,一般會導致程序崩潰。其中胁赢,Linux專門提供了一類crash信
號企蹭,在程序接收到此類信號時,缺省操作是將崩潰的現(xiàn)場信息記錄到核心文件智末,然后終止進程谅摄。
常見崩潰信號列表:
SIGSEGV
內(nèi)存引用無效。
SIGBUS
訪問內(nèi)存對象的未定義部分系馆。
SIGFPE
算術(shù)運算錯誤螟凭,除以零。
SIGILL
非法指令它呀,如執(zhí)行垃圾或特權(quán)指令
SIGSYS
糟糕的系統(tǒng)調(diào)用
SIGXCPU
超過CPU時間限制。
SIGXFSZ
文件大小限制棒厘。
一般的出現(xiàn)崩潰信號纵穿,Android系統(tǒng)默認缺省操作是直接退出我們的程序。但是系統(tǒng)允許我們給某一個
進程的某一個特定信號注冊一個相應的處理函數(shù)(signal)奢人,即對該信號的默認處理動作進行修改谓媒。因
此NDK Crash的監(jiān)控可以采用這種信號機制,捕獲崩潰信號執(zhí)行我們自己的信號處理函數(shù)從而捕獲NDK
Crash何乎。
BreakPad
Google breakpad是一個跨平臺的崩潰轉(zhuǎn)儲和分析框架和工具集合句惯,其開源地址是:https://github.co
m/google/breakpad。breakpad在Linux中的實現(xiàn)就是借助了Linux信號捕獲機制實現(xiàn)的支救。因為其實現(xiàn)
為C++抢野,因此在Android中使用,必須借助NDK工具各墨。
引入項目
將Breakpad源碼下載解壓指孤,首先查看README.ANDROID文件。
If you're using the ndk-build build system, you can follow
these simple steps:
1/ Include android/google_breakpad/Android.mk from your own
project's Android.mk
This can be done either directly, or using ndk-build's
import-module feature.
2/ Link the library to one of your modules by using:
LOCAL_STATIC_LIBRARIES += breakpad_client
NOTE: The client library requires a C++ STL implementation,
which you can select with APP_STL in your Application.mk
It has been tested succesfully with both STLport and GNU libstdc++
使用步驟:
1.將
android/google_breakpad/Android.mk 導入到我們自己項目中;
2.在自己的Android.mk中依賴breakpad_client
第一步:我們先新建一個module命名為breakpad恃轩;
第二步:在該module下的main目錄新建cpp文件夾结洼;
第三步:將Breakpad下載解壓后的src拷貝至cpp包下面;
第四步:在Breakpad下與src同級目錄下新建CMakeLists.txt叉跛,并編寫編譯內(nèi)容松忍,該內(nèi)容來源于android/google_breakpad/Android.mk文件,但是需要用cmake語法來完成編譯腳本筷厘,具體內(nèi)容如下:
cmake_minimum_required(VERSION 3.4.1)
include_directories(src src/common/android/include)
enable_language(ASM)
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)
target_link_libraries(breakpad log)
第五步:在cpp目錄下新建breakpad.cpp文件鸣峭,編寫breakpad crash監(jiān)控代碼,具體內(nèi)容如下敞掘;
//
// Created by Administrator on 2020/11/5.
//
#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, "native", "native crash:%s", descriptor.path());
return false;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_breakpad_crash_BreakpadCrash_initNativeCrash(JNIEnv *env, jclass clazz, jstring path_) {
const char *path = env->GetStringUTFChars(path_, 0);
__android_log_print(ANDROID_LOG_INFO, "native", "===> %s", path);
google_breakpad::MinidumpDescriptor descriptor(path);
static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback,
NULL, true, -1);
env->ReleaseStringUTFChars(path_, path);
}
第六步:在cpp目錄新建CMakeLists.txt來將breakpad庫和crash 監(jiān)控編譯為可移植的so叽掘;
cmake_minimum_required(VERSION 3.4.1)
include_directories(breakpad/src breakpad/src/common/android/include)
# libbugly.so
add_library(breakpadcrash-lib SHARED breakpad.cpp)
add_subdirectory(breakpad)
# 鏈接ndk中的log庫
target_link_libraries(breakpadcrash-lib breakpad log)
第七步:新建java文件調(diào)用so 的native方法
public class BreakpadCrash {
static {
System.loadLibrary("breakpadcrash-lib");
}
public static void init(Context context) {
Context applicationContext = context.getApplicationContext();
File file = new File(applicationContext.getExternalCacheDir(), "native_crash");
if (!file.exists()) {
file.mkdirs();
}
initNativeCrash(file.getAbsolutePath());
}
public static native void initNativeCrash(String file);
}
第八步:在build.gradle文件中添加編譯配置
apply plugin: 'com.android.library'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
externalNativeBuild {
cmake {
// cppFlags "-std=c++11"
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.2.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
第九步:之后build該模塊就可以在build\intermediates\cmake目錄中看到生成的so
使用:在Application的oncreate中調(diào)用
另外我們需要新建一個moudle來模擬第三方的jni so庫,然后在activity中使用該測試庫實現(xiàn)崩潰玖雁;
應用崩潰之后可以在data.包名.cache.native下面找到日志文件更扁,改文件為dmp文件,需要使用minidump_stackwalk 工具來編寫為正常的日志文件赫冬,在 Android Studio 的安裝目錄下的 bin\lldb\bin 里面就存在一
個對應平臺的 minidump_stackwalk浓镜,進入該目錄或者配置該目錄到環(huán)境變量,然后執(zhí)行:
minidump_stackwalk xxxx.dump > crash.txt
打開txt文件可以看到:
有對應的錯誤類型和堆棧信息劲厌。
接下來使用 Android NDK 里面提供的 addr2line 工具將寄存器地址轉(zhuǎn)換為對應符號膛薛。addr2line 要用和
自己 so 的 ABI 匹配的目錄,同時需要使用有符號信息的so(一般debug的就有)补鼻。
因為我使用的是模擬器x86架構(gòu)哄啄,因此addr2line位于:
E:\sdk\ndk\21.1.6352462\toolchains\x86_64-4.9\prebuilt\windows-x86_64\bin
x86_64-linux-android-addr2line.exe -f -C -e E:\workspace\AndroidCrash\jnibug\build\intermediates\cmake\debug\obj\x86\libbugly-lib.so 0x5a4