Android异常监控
Crash就是由于代码异常而导致App非正常退出现象,也就是我们常说的『崩溃』
通常情况下会有以下两种类型Crash:
- Java Crash
- Native Crash
Java Crash
Java的Crash监控非常简单,Java中的Thread定义了一个接口: UncaughtExceptionHandler ;用于
处理未捕获的异常导致线程的终止(注意:****catch****了的是捕获不到的),当我们的应用crash的时候,就
会走 UncaughtExceptionHandler.uncaughtException ,在该方法中可以获取到异常的信息,我们通
过 Thread.setDefaultUncaughtExceptionHandler 该方法来设置线程的默认异常处理器,我们可以
将异常信息保存到本地或者是上传到服务器,方便我们快速的定位问题。
Android系统崩溃处理的流程:
图片来源:https://xiang-yu.blog.csdn.net/article/details/106647428
具体的处理过程可参考:https://xiang-yu.blog.csdn.net/article/details/106647428
ZygoteInit每次fork出一个进程之后,会先初始化日志、crash监控,再利用反射调用 ActivityThread 的main()方法启动app。那么我们可以自己的app中重新注册Crash监控回调接口,然后在自己的app中处理Crash。
具体的实现类如下:
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();
}
/**
* 当程序中有未被捕获的异常,系统将会调用这个方法
*
* @param t 出现未捕获异常的线程
* @param e 得到异常信息
*/
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
//自行处理:保存本地
File file = dealException(t, e);
//上传服务器
//......
} catch (Exception e1) {
e1.printStackTrace();
} finally {
//交给系统默认程序处理
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);
//往文件中写入数据
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架构
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信号一方面用于正常的进程间通信和同步,另一方
面它还负责监控系统异常及中断。当应用程序运行异常时,Linux内核将产生错误信号并通知当前进
程。当前进程在接收到该错误信号后,可以有三种不同的处理方式。
忽略该信号;
捕捉该信号并执行对应的信号处理函数(信号处理程序);
执行该信号的缺省操作(如终止进程);
当Linux应用程序在执行时发生严重错误,一般会导致程序崩溃。其中,Linux专门提供了一类crash信
号,在程序接收到此类信号时,缺省操作是将崩溃的现场信息记录到核心文件,然后终止进程。
常见崩溃信号列表:
SIGSEGV
内存引用无效。
SIGBUS
访问内存对象的未定义部分。
SIGFPE
算术运算错误,除以零。
SIGILL
非法指令,如执行垃圾或特权指令
SIGSYS
糟糕的系统调用
SIGXCPU
超过CPU时间限制。
SIGXFSZ
文件大小限制。
一般的出现崩溃信号,Android系统默认缺省操作是直接退出我们的程序。但是系统允许我们给某一个
进程的某一个特定信号注册一个相应的处理函数(signal),即对该信号的默认处理动作进行修改。因
此NDK Crash的监控可以采用这种信号机制,捕获崩溃信号执行我们自己的信号处理函数从而捕获NDK
Crash。
BreakPad
Google breakpad是一个跨平台的崩溃转储和分析框架和工具集合,其开源地址是:https://github.co
m/google/breakpad。breakpad在Linux中的实现就是借助了Linux信号捕获机制实现的。因为其实现
为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,并编写编译内容,该内容来源于android/google_breakpad/Android.mk文件,但是需要用cmake语法来完成编译脚本,具体内容如下:
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监控代码,具体内容如下;
//
// Created by Administrator on 2020/11/5.
//
#include
#include
#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 监控编译为可移植的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文件调用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中调用
另外我们需要新建一个moudle来模拟第三方的jni so库,然后在activity中使用该测试库实现崩溃;
应用崩溃之后可以在data.包名.cache.native下面找到日志文件,改文件为dmp文件,需要使用minidump_stackwalk 工具来编写为正常的日志文件,在 Android Studio 的安装目录下的 bin\lldb\bin 里面就存在一
个对应平台的 minidump_stackwalk,进入该目录或者配置该目录到环境变量,然后执行:
minidump_stackwalk xxxx.dump > crash.txt
打开txt文件可以看到:
有对应的错误类型和堆栈信息。
接下来使用 Android NDK 里面提供的 addr2line 工具将寄存器地址转换为对应符号。addr2line 要用和
自己 so 的 ABI 匹配的目录,同时需要使用有符号信息的so(一般debug的就有)。
因为我使用的是模拟器x86架构,因此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
项目源码:https://github.com/heezier/AndroidCrash