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

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

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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可以判断崩溃线程从而实现多线程情况下的跳转恢复,本地测试运行正常。待线上检测效果。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值