对于Android来说,一般自定一个Hanlder就可以捕获Java层的崩溃,然后可以轻松实现崩溃信息上传,而且添加额外的信息,这也是大多数商用/开源SDK能实现的。
但对于Native的崩溃,这些只有纯Java编写的SDK显得很无力。所以一般会采用C/C++来实现对当时崩溃的内存进行dump操作。本文将介绍使用breakpad和一个轻量级的http库来是实现,native崩溃日志生成和崩溃时上传。

为什么需要自己来写

  1. 腾讯的bugly在两年前,我们boss就说这个项目腾讯可能不再维护,实际也证明了这一点,至少我们之前在bugly添加的升级现在不可用的
  2. 知道腾讯bugly不可用后,我们将各个平台(后端,前端,Android OS端,Android移动端)的错误日志搜集的迁移到自建的Sentry平台,实现了,错误日志的同一管理,但是这个平台的查询就Android来说并不是太方便。
  3. 官方的sentry-native当前处于不能用的阶段,生成的so个数过多,颗粒度,太细,还有特别是在上传部分,使用libcurl,这个库太大,而且还依赖zlib.so.1什么的
  4. 官方的native-sdk非CMake项目不好调试

sentry native crash上报原理

Sentry native crash上报原理:利用breakpad或者crshpad捕获崩溃,生成minidump文件,然后将文件上传至sentry处理这个minidump文件的接口,接口地址在Sentry平台下的Settings–>Projects–>your_project_name–>Client Keys(DSN)–>Expand–>Minidump Endpoint下

API示例

1
2
3
4
$ curl -X POST \
'https://sentry.io/api/<project>/minidump/?sentry_key=<key>' \
-F upload_file_minidump=@mini.dmp \ #必须是@+文件名(不包含路径的文件名)
-F 'sentry={"release":"1.2.3","tags":{"mytag":"value"}}'

官方对native SDK还有一些其他的功能,例如自定义消息上报,但是这个是埋点系统做的,对于崩溃来说,我们需要知道的额外的信息有两类

  1. 关于与用户相关信息,例如用户名,用户所用的设备序列号(后续可以接和埋点系统查询崩溃时操作的上下文)
  2. 应用相关的信息,版本信息

项目合并和移植

为方便引用的使用git submouble的形式引入breakpad和libnhr
libnhr : https://github.com/CFM880/libnhr
breakpad : https://github.com/google/breakpad

1
2
3
4
5
6
7
8
9
├── app  #demo 目录
├── build.gradle
├── gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
├── local.properties
├── sentrynativereport # 自定义c++ lib目录
└── settings.gradle

sentrynativereport/src/main/cpp目录下

1
2
3
4
5
├── CMakeLists.txt
├── breakpad # breakpad源路径
├── libnhr # 修改后的libnhr
├── sentry-report-impl.cpp # 提供jni实现接口
└── util #log 和url_parser

.gitmodules

1
2
3
4
5
6
[submodule "sentrynativereport/src/main/cpp/breakpad"]
path = sentrynativereport/src/main/cpp/breakpad
url = https://github.com/google/breakpad.git
[submodule "sentrynativereport/src/main/cpp/libnhr"]
path = sentrynativereport/src/main/cpp/libnhr
url = https://github.com/CFM880/libnhr.git

sentrynativereport/src/main/cpp/CMakeLists.txt内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
include_directories(
breakpad/src/common/android/include
breakpad/src/common/android
breakpad/src
libnhr/src
libnhr/
util/

)

# ASM文件特殊处理一下
file(GLOB BREAKPAD_ASM_SOURCE breakpad/src/common/android/breakpad_getcontext.S)
set_source_files_properties(${BREAKPAD_ASM_SOURCE} PROPERTIES LANGUAGE C)

add_library( # Sets the name of the library.
sentry-report

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
sentry-report-impl.cpp
breakpad/src/client/linux/crash_generation/crash_generation_client.cc
breakpad/src/client/linux/dump_writer_common/thread_info.cc
breakpad/src/client/linux/dump_writer_common/ucontext_reader.cc
breakpad/src/client/linux/handler/exception_handler.cc
breakpad/src/client/linux/handler/minidump_descriptor.cc
breakpad/src/client/linux/log/log.cc
breakpad/src/client/linux/microdump_writer/microdump_writer.cc
breakpad/src/client/linux/minidump_writer/linux_dumper.cc
breakpad/src/client/linux/minidump_writer/linux_ptrace_dumper.cc
breakpad/src/client/linux/minidump_writer/minidump_writer.cc
breakpad/src/client/minidump_file_writer.cc
breakpad/src/common/convert_UTF.cc
breakpad/src/common/md5.cc
breakpad/src/common/string_conversion.cc
breakpad/src/common/linux/elfutils.cc
breakpad/src/common/linux/file_id.cc
breakpad/src/common/linux/guid_creator.cc
breakpad/src/common/linux/linux_libc_support.cc
breakpad/src/common/linux/memory_mapped_file.cc
breakpad/src/common/linux/safe_readlink.cc
${BREAKPAD_ASM_SOURCE}

libnhr/src/nhr_common.c
libnhr/src/nhr_gz.c
libnhr/src/nhr_map.c
libnhr/src/nhr_memory.c
libnhr/src/nhr_request_method_common.c
libnhr/src/nhr_request_method_get.c
libnhr/src/nhr_request_method_post.c
libnhr/src/nhr_request_private.c
libnhr/src/nhr_request_public.c
libnhr/src/nhr_response.c
libnhr/src/nhr_string.c
libnhr/src/nhr_thread.c

util/XLog.h
util/url_parser.c
)
find_library(
log-lib
log)

target_link_libraries(
sentry-report
${log-lib})

sentry-report-impl.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const char *report_minidump_url;
const char *entry_info;
bool DumpCallback(const google_breakpad::MinidumpDescriptor &descriptor,
void *context,
bool succeeded) {
XLOGD("===============crashed================");
XLOGD("Dump path: %s\n", descriptor.path());
if (succeeded && report_minidump_url != nullptr) {
XLOGD("report url=%s\n", report_minidump_url);
test_post_number(descriptor.path());
}
return succeeded;
}



extern "C"
JNIEXPORT void JNICALL
Java_com_spax_sentrynativereport_SentryReport_initReport(JNIEnv *env, jclass clazz,
jstring dump_dir,
jstring report_url,
jstring extra_Info) {
const char *path = env->GetStringUTFChars(dump_dir, 0);
report_minidump_url = env->GetStringUTFChars(report_url, 0);
if (extra_Info != nullptr) {
entry_info = env->GetStringUTFChars(extra_Info, 0);
}
XLOGD("init %d\n", std::this_thread::get_id());
google_breakpad::MinidumpDescriptor descriptor(path);
static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback, NULL, true, -1);
env->ReleaseStringUTFChars(dump_dir, path);
}

其中的缺省的方法参考
https://github.com/CFM880/libnhr/blob/master/tests/test_post.c

其中url拼接,添加file上传参数和自定义sentry参数方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
test_post_request = nhr_request_create();
// 解析http地址
parsed_url *url = parse_url(report_minidump_url);

unsigned short port = 80;
if (url->port != NULL) {
port = atoi(url->port);
}
// 设置url
std::string start = "/";
std::string stringPath = url->path;
std::string stringQuery = url->query;
std::string stringQuery1 = "?";
std::string uploadPath = start + stringPath + stringQuery1 + stringQuery;
const char *ur = uploadPath.data();
XLOGD("path %s",ur);
nhr_request_set_url(test_post_request, url->scheme, url->host, ur, port);

nhr_request_set_method(test_post_request, nhr_method_POST);
nhr_request_set_timeout(test_post_request, 10);


// 读取文件
FILE *file = fopen(file_path, "rb");
fseek(file, 0, SEEK_END);
int inputFileLength = ftell(file);
printf("size:%d\n",inputFileLength);
rewind(file);
char *ar = (char *)malloc(sizeof(char)*(inputFileLength + 1));
if (!ar)
return -1;
fread(ar, inputFileLength,1, file);



// 拼接文件名
char *file_name = basename(file_path);
std::string startFile = "@";
std::string filePath = file_name;
std::string realPath = startFile + filePath;
const char *realPathChar = realPath.data();
XLOGD("file name %s",realPathChar);
nhr_request_add_data_parameter(test_post_request, "upload_file_minidump", realPathChar, ar, inputFileLength);
if (entry_info != nullptr) {
nhr_request_add_data_parameter(test_post_request, "sentry", "", entry_info, strlen(entry_info));
}
..........
free(ar);
fclose(file);

JNI接口SentryReport

1
2
3
4
5
6
7
8
package com.spax.sentrynativereport;

public class SentryReport {
static {
System.loadLibrary("sentry-report");
}
public static native void initReport(String dumpDir, String reportUrl, String extraInfo);
}

至此项目合并和移植完成
效果
Android Studio
Sentry Platform

中间遇到坑

  1. 如何将Android.mk项目迁移到CMake项目;Android.mk中的源文件发到CMakeList.txt的源文件下,ASM怎样编译到项目中。

  2. 第一次尝试在jni回调到Java层来实现上传,但是这时候整JVM都快挂了,类可能已被卸载,也可能查找这个类的时候JVM环境已被破坏,找不到类。

  3. 文件读取坑;对于读文件的不熟悉,导致上传文件到服务时,服务端文件md5改变,证明这问题,还搭建了一个spring-boot文件上传项目
  4. 上传文件:又由于文档并未说明upload_file_minidump=@mini.dmp,文件名不能带路径,有被坑了好久。
  5. 添加额外的属性值;之前提得到libnhr并未实现我想要的结果,需要改源码,如何发现不一样的呢,通过在Android tcpdump抓包,发现,正常的传属性值是没有filename这个字段。
  6. 为了证明不是我们测试服务的问题,还搭建一个sentry的docker环境

我的问题

  1. 对C语言的不熟悉,导致读文件占用时间比较长
  2. 对tcp协议的实现http不熟悉,用wireshark对比正确的协议与错误的协议