【Android native crash崩溃捕获恢复的多线程支持】

文章介绍了在Android中如何利用sigsetjmp和siglongjmp实现JNI层的崩溃捕获,以模拟Java的try-catch机制。然而,这种方法在多线程环境下可能无法捕获所有崩溃。作者提出通过线程id来识别崩溃线程,改进了崩溃恢复机制,以提高捕获效率。实际测试表明,该方法在本地运行正常,等待线上验证效果。

Android native crash catch

问题

基于第三方so库的JNI开发,空指针和野指针问题防不胜防。参考网上基于sigsetjmp和siglongjmp实现native崩溃捕获,然后返回接口失败值,实现了类似Java里的try-catch功能。

参考:聊一聊应用层开发者怎么应对Native Crash:https://juejin.cn/post/7178341941480783931/

实际上线跑了两周发现捕获崩溃只能挽回50%崩溃,还有一些莫名其妙的崩溃基本没有堆栈信息。猜测是多线程情况下捕获跳转错误,导致程序状态混乱而再次崩溃。

方案

由于在多线程情况下,崩溃信号捕获处理时,很难判断是哪个方法崩溃。从崩溃信息捕获入手,如果打印信息可以打印错误堆栈,那么从错误堆栈比较,回溯到我们调用sigsetjmp的方法,就可以找到siglongjmp跳转回来的位置。
堆栈从哪里获取呢?
参考xCrash 源码:http://blog.itpub.net/69945252/viewspace-2674668/
堆栈获取方式复杂,而且平台相关,工作量大。

在看Java堆栈获取时,意外发现线程id可回溯到崩溃线程,于是用线程id作为线程标志,从而判断崩溃点。
经测试发现,信号处理方法,是在崩溃线程。直接上代码:
参数定义如下


static struct sigaction old; // 旧信号处理

typedef struct {
    int tid = 0; // 当前线程id
    int isCrashFlag = 0; //是否上报crash标志
    sigjmp_buf sig_env; // 跳转堆栈
} signal_catch_info_t;

// 初始化三个缓存数据
static signal_catch_info_t signal_catch_info[] =
        {
                {},
                {},
                {}
        };

static int signal_catch_count = 0; // 缓存计数
static volatile int signal_catch_index = 0; // 当前缓存索引

信号注册和拦截方法

/**
 * 信号拦截跳转方法
 * @param sig
 */
static void sigsegv_handler(int sig) {
    int crash_tid = gettid();
    for (int i = 0; i < signal_catch_count; i++) {
        if (signal_catch_info[i].tid == crash_tid) {
            signal_catch_info[i].isCrashFlag = 1;
            siglongjmp(signal_catch_info[i].sig_env, 1);
            return;
        }
    }
    // 交给原来的信号处理器处理
    sigaction(sig, &old, NULL);
}

/**
 * 初始化信号拦截
 */
extern "C" jint JNI_OnLoad(JavaVM *vm, void *reserved) {

    signal_catch_count = sizeof(signal_catch_info) / sizeof(signal_catch_info[0]);

    sigset_t block_mask;
    sigemptyset(&block_mask);
    sigaddset(&block_mask, SIGABRT); // handler处理捕捉到的信号量时,需要阻塞的信号
    sigaddset(&block_mask, SIGSEGV); // handler处理捕捉到的信号量时,需要阻塞的信号
    sigaddset(&block_mask, SIGBUS); // handler处理捕捉到的信号量时,需要阻塞的信号
    sigaddset(&block_mask, SIGFPE); // handler处理捕捉到的信号量时,需要阻塞的信号

    struct sigaction sigc;
    sigc.sa_handler = sigsegv_handler;
    sigemptyset(&sigc.sa_mask);
    sigc.sa_flags = SA_SIGINFO;
    sigc.sa_mask = block_mask;
    sigaction(SIGSEGV, &sigc, &old);
    sigaction(SIGABRT, &sigc, &old);
    sigaction(SIGBUS, &sigc, &old);
    sigaction(SIGFPE, &sigc, &old);
    return JNI_VERSION_1_4;
}

异常捕获设置和清除,以及上报到Java

/**
 * 上报崩溃信息到Java
 * @param env
 * @param arg
 */
static bool reportCrashMessage(JNIEnv *env, jstring arg) {
    int tid = gettid();
    for (int i = 0; i < signal_catch_count; i++) {
        if (signal_catch_info[i].tid == tid) {
            signal_catch_info[i].isCrashFlag = 0;
            StructureDebugCallback(env);
            DebugCallbackReportError(env, arg);
            DestroyDebugCallback(env);
            return JNI_TRUE;
        }
    }
    return JNI_FALSE;
}

/**
 * 清除捕获异常
 */
extern "C" jboolean cleanCatch() {
    int tid = gettid();
    for (int i = 0; i < signal_catch_count; i++) {
        if (signal_catch_info[i].tid == tid) {
            signal_catch_info[i].tid = 0;
            signal_catch_info[i].isCrashFlag = 0;
        }
    }
    return JNI_TRUE;
}

/**
 * 设置捕获异常
 */
extern "C" jboolean setCatch() {
    int index = signal_catch_index++ % signal_catch_count;
    signal_catch_info[index].tid = gettid();
    signal_catch_info[index].isCrashFlag = 0;
    if (sigsetjmp(signal_catch_info[index].sig_env, 1)) {
        __android_log_print(ANDROID_LOG_INFO, "pdfelement_jni", "%s",
                            "setCatch --- crash by catch.");
        return JNI_TRUE;
    }
    return JNI_FALSE;
}

最后是使用

 if (setCatch() == JNI_FALSE) {
        jint result = UnknownRelease(native_ptr);
        if (reportCrashMessage(env, env->NewStringUTF("nativeRelease crash"))) {
            result = 0;
        }
        cleanCatch();
        return result;
    }

正常运行流程

  1. setCatch() 返回false。
  2. 业务逻辑:调用底层接口UnknownRelease并返回。
  3. reportCrashMessage 返回FALSE。
  4. cleanCatch 清除本次捕获。

异常执行流程

  1. 业务逻辑:调用底层接口UnknownRelease崩溃未返回。
  2. 信号被捕获,回调 sigsegv_handler 。
  3. 循环判断到当前线程id,执行siglongjmp。
  4. 代码跳转到业务逻辑下一行:reportCrashMessage判断。
  5. reportCrashMessage方法里上报异常。
  6. reportCrashMessage方法返回后设置失败值。
  7. cleanCatch清除捕获设置。
  8. return返回失败值。

总结

通过线程id可以判断崩溃线程从而实现多线程情况下的跳转恢复,本地测试运行正常。待线上检测效果。

Android 开发中,**捕获 Native 层的 Crash** 是指捕获发生在 C/C++ 层(即 Native Code)中的崩溃,例如由 JNI 代码、NDK 编写的 C++ 逻辑、底层系统调用等引发的崩溃。这类崩溃通常不会被 Java 层的 `UncaughtExceptionHandler` 捕获,因此需要专门的机制来捕获和上报。 --- ## ✅ 一、NativeCrash 的类型 | 类型 | 描述 | |------|------| | **SIGSEGV** | 段错误,访问非法内存地址 | | **SIGABRT** | 主动调用 abort() 或 assert 失败 | | **SIGFPE** | 浮点异常,如除以零 | | **SIGILL** | 执行非法指令 | | **SIGBUS** | 总线错误,如未对齐访问 | --- ## ✅ 二、如何捕获 NativeCrash ### 1. 使用 `signal()` 或 `sigaction()` 设置信号处理器(C/C++ 层) 你可以通过注册信号处理器来捕获常见的崩溃信号,并在崩溃时执行自定义逻辑(如记录日志、生成堆栈信息)。 ```cpp #include <signal.h> #include <stdio.h> #include <stdlib.h> void signal_handler(int signal) { printf("Caught signal %d\n", signal); // 可以在此处记录日志、上传堆栈等 // 注意:此处不能调用复杂的函数(如 malloc、printf 等) exit(signal); } extern "C" JNIEXPORT void JNICALL Java_com_example_app_NativeLib_registerHandlers(JNIEnv *env, jobject /* this */) { signal(SIGSEGV, signal_handler); signal(SIGABRT, signal_handler); signal(SIGFPE, signal_handler); signal(SIGILL, signal_handler); } ``` ⚠️ 注意: - 不能在信号处理函数中调用复杂的 C++ 函数; - 不推荐在此处做大量逻辑处理,建议记录崩溃信息后退出进程。 --- ### 2. 使用 Google Breakpad / Crashpad Google Breakpad 和其继任者 **Crashpad** 是 Google 开源的跨平台崩溃收集库,支持 Android 平台,可以捕获 Native 层的完整堆栈并生成 minidump 文件。 #### ✅ Crashpad 的优势: - 支持 Native 崩溃堆栈捕获; - 支持自定义上传; - 支持符号化还原; - 支持多线程堆栈收集。 #### 使用步骤(简化): 1. 编译并集成 Crashpad 到 Android 项目; 2. 初始化 Crashpad Handler; 3. 配置 Dump 上传路径; 4. 崩溃时生成 minidump 文件并上传。 --- ### 3. 使用 Android 的 tombstone 机制(系统级) Android 系统会在 Native 崩溃时生成 `tombstone` 文件,通常位于 `/data/tombstones/` 目录下。这些文件包含了崩溃时的寄存器状态、堆栈等信息。 #### 获取 tombstone 文件的方式: - 设备 root 后手动读取; - 通过 `adb logcat` 查看崩溃日志; - 通过 `logcat` 中的 `FATAL` 错误和 `backtrace` 信息初步分析。 ```bash adb logcat -b crash ``` --- ### 4. 使用 Firebase Crashlytics(支持 Native) Firebase Crashlytics 从版本 18.1.0 开始支持 Native 崩溃捕获(通过集成 Crashpad)。 #### 集成步骤: 1. 在 `build.gradle` 中添加 Crashlytics 依赖; 2. 配置 NDK 支持; 3. 构建时上传符号表(Symbol File); 4. 崩溃发生后可在 Firebase 控制台查看 Native 堆栈。 --- ## ✅ 三、Native 崩溃的分析工具 | 工具 | 支持平台 | 说明 | |------|----------|------| | **addr2line** | Linux / Android | 将地址转换为源码行号 | | **ndk-stack** | Android | 将 tombstone 堆栈转换为可读堆栈 | | **gdb** | Linux | 使用 GDB 调试 minidump 文件 | | **Crashpad / Breakpad** | Android / iOS / Linux | 本地生成 minidump | | **Firebase Crashlytics** | Android / iOS | 支持 Native 崩溃上报和符号化 | | **Sentry** | Android / iOS / Web | 支持 Native 崩溃捕获与分析 | --- ## ✅ 四、如何还原 Native 崩溃堆栈 1. **获取 minidump 或 tombstone 文件**; 2. **使用符号表(symbol file)进行还原**; 3. 工具如 `minidump_stackwalk`(Breakpad)或 `crashpad_dump`(Crashpad)进行分析; 4. 结合源码和编译信息定位问题。 --- ## ✅ 五、总结建议 | 场景 | 推荐方案 | |------|----------| | 本地调试 | 使用 `logcat` + `tombstone` + `ndk-stack` | | 测试环境 | 集成 Crashpad / Breakpad | | 线上监控 | 使用 Firebase Crashlytics 或 Sentry | | 日志收集 | 自定义信号处理 + 日志上传 | | 崩溃分析 | 使用 `addr2line`、`ndk-stack`、符号表还原堆栈 | ---
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值