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  
#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, "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

你可能感兴趣的:(Crash 监控)