问题
基于第三方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;
}
正常运行流程:
- setCatch() 返回false。
- 业务逻辑:调用底层接口UnknownRelease并返回。
- reportCrashMessage 返回FALSE。
- cleanCatch 清除本次捕获。
异常执行流程:
- 业务逻辑:调用底层接口UnknownRelease崩溃未返回。
- 信号被捕获,回调 sigsegv_handler 。
- 循环判断到当前线程id,执行siglongjmp。
- 代码跳转到业务逻辑下一行:reportCrashMessage判断。
- reportCrashMessage方法里上报异常。
- reportCrashMessage方法返回后设置失败值。
- cleanCatch清除捕获设置。
- return返回失败值。
总结
通过线程id可以判断崩溃线程从而实现多线程情况下的跳转恢复,本地测试运行正常。待线上检测效果。