做Android开发的同学,基本上都遇到过程序崩溃,大部分的崩溃问题都存在于Java层。在开发过程中,如果遇到崩溃,我们可以在logcat中找到相关的信息进行修改;如果是线上出现的问题,我们可以使用Bugly,友盟等三方工具进行错误上报,或者自己做监控,生成错误文件进行上报分析,这时候需要用到UncaughtExceptionHandler接口
如果是Native层发生的崩溃怎么办?基本这个问题我们都会略过,第一:看不懂,第二:三方的so库发生了问题我们也解决不了。所以Native层的崩溃监听与解析,主要针对Native开发的同学。我们从一下几个方面来介绍:
目录
一、解析
二、拦截与解析
三、踩坑
四、解析工具的编译
1.工具的获取
2.解析dump文件
一、解析
如果是在开发过程中,发生了Native的报错,在Logcat中会有相关信息的输出,如下:
Fatal signal 11 (SIGSEGV), code 1, fault addr 0x0 in tid 14943 (c.myapplication)
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Revision: '0'
ABI: 'arm'
pid: 14943, tid: 14943, name: c.myapplication >>> com.gzc.myapplication <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Cause: null pointer dereference
r0 0000003d r1 00000000 r2 0000000a r3 00000003
r4 cf0ccd9f r5 00000000 r6 00000000 r7 ff97c7b8
r8 00000056 r9 f224b000 sl 00000000 fp ff97c834
ip ff97c2c0 sp ff97c790 lr cf0a0fa9 pc cf0a0fac cpsr 200f0030
backtrace:
#00 pc 0001efac /data/app/com.gzc.myapplication-ZnQLsODtIAPR0wRmY-jkmw==/lib/arm/libnativecrash.so (Java_com_gzc_myapplication_NativeCrashTest_testCrash+75)
#01 pc 0001e127 /data/app/com.gzc.myapplication-ZnQLsODtIAPR0wRmY-jkmw==/oat/arm/base.odex (offset 0x1e000)
上面是我模拟的Native崩溃时,输出的日志:
第6行:这里是Linux系统的段信号,这个信号值表示了此时发生错误的类型是SIGSEGV,而Linux中常见的错误信号如下:
信号 | 描述 |
SIGSEGV | 内存引用无效 |
SIGBUS | 访问内存对象的未定义部分 |
SIGFPE | 算术运算错误,例如除以零 |
SIGILL | 非法指令,如执行垃圾或特权指令 |
SIGSYS | 糟糕的系统调用 |
SIGXCPU | 超过CPU时间限制 |
SIGXFSZ | 文件大小限制 |
所以这里的错误应该是和内存相关的,而第6行的fault addr 0x0也提示着我们是空指针问题。
第13行:这一行中,告诉了我们发生错误的so库是libnativecrash.so,方法也指出了了是Java_com_gzc_myapplication_NativeCrashTest_testCrash方法,但是并没有指名是哪一行,如果这个方法少,我们根据这些信息还有段信号能找到问题所在,如果方法的内容比较多的时候,不知道具体的报错行数,对于我们寻找错误是比较难搞的。这一行还有一个比较关键的值,就是pc单词后面的值0001efac,这个值是内存地址。而Android NDK提供了一个工具addr2line,可以将这个内存地址转化为具体的报错行数。
addr2line在Android sdk的目录中,如下
Android/sdk/ndk/21.4.7075529/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line
根据相应的cpu架构去找就可以了
找到后,输入如下的指令(so库是自己的,所以要填上具体的so库枯井):
aarch64-linux-android-addr2line -f -C -e libnativecrash.so 0001efac
结果
Java_com_gzc_myapplication_NativeCrashTest_testCrash
...../app/src/main/cpp/nativecrashlib.cpp:14
提示了具体的方法和代码行数
二、拦截与解析
如果是线上发生的错误,我们应该怎么办?当然使用Bugly和友盟是最好的办法。但是他们基本的原理我们还是要知道的,而且就像Java中的UncaughtExceptionHandler一样,我们可能去拦截错误,去自定义行为等。
Google提供了一个跨平台的崩溃转储以及分析框架,叫做BreakPad。BreakPad在Linux中的实现,就是借助了Linux信号捕获机制实现的,因为BreakPad的实现使用的是C++,所以在Android中使用,需要用到NDK工具。
首先,我们将BreakPad的源码下载下来(地址:https://github.com/google/breakpad):
目录如下:
标注1:我们看一下这个文件夹
这里有一个google_breakpad文件夹,里面有一个文件类型为mk的文件。做Native开发的同学,对这个文件应该不陌生,不过现在都已经使用CMakeLists.txt了。我们现在要做的就是将mk的内容,转化为CMakeLists.txt内容,移植到我们自己的项目里。
标注2: 这个文件还是要好好的读一下,这里介绍了如何在Android 项目中使用BreakPad,当然我也会一步步的去介绍,其中也会有一些坑帮大家踩
标注3:源代码,我们需要将这个文件夹导入到我们自己的项目中,如下
cpp这个目录就不说了,做Native层开发的同学应该都知道怎么在gradle中去配,不知道小伙伴,留言哈
我们在cpp中创建breakpad文件夹(名称随便起),将标注3中src的文件夹导入到这里,然后再breakpad中创建CMakeLists.txt,而这个CMakeLists.txt就是标注1中所说的,Android.mk转化的内容,我们先看标注1中的Android.mk文件
LOCAL_PATH := $(call my-dir)/../..
include $(CLEAR_VARS)
LOCAL_MODULE := breakpad_client
LOCAL_CPP_EXTENSION := .cc
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)
LOCAL_EXPORT_LDLIBS := -llog
#编译为静态库
include $(BUILD_STATIC_LIBRARY)
# Done.
其实主要就是做了两件事:需要编译的文件以及导入头文件,还有一个要注意的点:在编译的源文件中,有一个后缀为.S的文件,这个文件是汇编文件,我们需要在CMakeLists.txt进行配置,内容如下:
cmake_minimum_required(VERSION 3.18.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)
这个CMakeLists.txt的内容还有个坑,我们先一步一步的往下走
写完这个CMakeLists.txt内容后,我们还要在cpp目录下的CMakeLists.txt中进行配置,将刚刚的这个CMakeLists.txt引入进去
cmake_minimum_required(VERSION 3.18.1)
#引入头文件
include_directories(breakpad/src breakpad/src/common/android/include)
add_library(nativecrash SHARED nativecrashlib.cpp)
#添加子目录,会自动查找这个目录下的CMakeList
add_subdirectory(breakpad)
target_link_libraries(nativecrash log breakpad)
注意:breakpad中的头文件,要再引入一次
之后我们在自己的native文件中对breakpad进行初始化,如下
#include <jni.h>
#include <string>
#include <android/log.h>
#include "breakpad/src/client/linux/handler/minidump_descriptor.h"
#include "breakpad/src/client/linux/handler/exception_handler.h"
//模拟崩溃
extern "C"
JNIEXPORT void JNICALL
Java_com_gzc_myapplication_NativeCrashTest_testCrash(JNIEnv *env, jclass clazz) {
__android_log_print(ANDROID_LOG_ERROR,"native","Java_com_gzc_myapplication_NativeCrashTest_testCrash");
int *p = NULL;
__android_log_print(ANDROID_LOG_ERROR,"native","Java_com_gzc_myapplication_NativeCrashTest_testCrash");
__android_log_print(ANDROID_LOG_ERROR,"native","Java_com_gzc_myapplication_NativeCrashTest_testCrash");
*p = 10;
}
//回调函数
bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
void* context,
bool succeeded) {
__android_log_print(ANDROID_LOG_ERROR,"native","DumpCallback");
printf("Dump path: %s\n", descriptor.path());
return false;
}
//breakpad初始化
extern "C"
JNIEXPORT void JNICALL
Java_com_gzc_myapplication_NativeCrashTest_initNative(JNIEnv *env, jclass clazz, jstring path_) {
const char* path = env->GetStringUTFChars(path_,0);
google_breakpad::MinidumpDescriptor descriptor(path);
static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback,
NULL, true, -1);
env->ReleaseStringUTFChars(path_,path);
}
注释我已经打好了,initNative需要接收Java层传过来的文件路径,用于崩溃时文件的生成
Java层的代码如下:
public class NativeCrashTest {
static {
System.loadLibrary("nativecrash");
}
public static void init(Context context){
Context applicationContext = context.getApplicationContext();
File file = new File(applicationContext.getExternalCacheDir(),"native_crash");
if(!file.exists()){
file.mkdirs();
}
//这里传的路径是崩溃时生成的文件路径
initNative(file.getAbsolutePath());
}
/**
* 模拟崩溃
*/
public static native void testCrash();
/**
* 初始化
* @param path
*/
private static native void initNative(String path);
}
三、踩坑
基本上完成了,但是还会遇到两个坑,这里也说一下
1.linux_syscall_support.h 文件照不到
Build的时候,会出现如下的错误
在third_party/lss中的文件找不到,我们发现,的确没有这个文件。其实这个文件需要我们单独去下载,然后添加进去,文件地址:https://chromium.googlesource.com/external/linux-syscall-support/+/refs/heads/master(需要翻墙,文章结尾我会给出自己已经下好的文件)
2.
这个文件添加进去之后,我们继续编译,还会报如下的错误
我点击了一下这个文件,发现这个文件其实是存在的
那为什么还是找不到呢?这个就是我上面说的,CMakeLists.txt中的坑,BreakPad提供的Android.mk文件里,所编译的源文件少了一个,就是这个pe_file.cc文件,我们添加上就行了,所以breakpad目录下完整的CMakeLists.txt如下:
cmake_minimum_required(VERSION 3.18.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/linux/minidump_writer/pe_file.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)
这时候进行Build是没有问题的
四、解析工具的编译
1.工具的获取
此时我们运行App,进行Native层的模拟编译,会在我们指定的目录生成崩溃文件,但是这个文件我们并看不了,需要使用解析工具对这个文件进行解析:
如果是Windows用户,直接在Android Studio的安装目录下的bin\lldb\bin中,找到minidump_stackwalk.exe文件,直接使用这个文件进行解析即可,指令我稍后说
如果是Mac用户,需要进行编译,才能获得这个minidump_stackwalk工具(有人说,在Mac的Android Studio的相同路径下也能找到,但是我的并没有)
首先,在终端窗口,cd到我们下载的breakpad路径下,执行如下指令:
./configure
这时候会在目录中生成Makefile文件,再执行make指令:
make
执行成功后,会在breakpad/src/processor目录中生成minidump_stackwalk文件
2.解析dump文件
输入如下指令:
./minidump_stackwalk my.dump > crash.txt
my.dump就是自己在手机导出来的崩溃文件;crash.txt是解析后的文件
内容如下:
和Logcat中的很像,关键代码是红框的部分,Thread 0 后面有一个crashed标识,说明这里是发生崩溃的线程,而下面就是崩溃的文件以及内存地址,使用上面说的addr2line工具进行解析就可以了