注册
ANR

黑科技!让Native Crash 与ANR无处发泄!

前言


高产似母猪的我,又带来了干货记录,本次是对signal的一个总结与回顾。不知道你们开发中,是否会遇到小部分的nativecrash 或者 anr,这部分往往是由第三方库导致的或者当前版本没办法修复的bug导致的,往往这些难啃的crash,对现有的crash数据指标造成一定影响,同时也对这小部分crash用户不友好,那么我们有没有办法实现一套crash or anr重启机制呢?其实是有的,相信在各个大厂都有一套“安全气囊”装置,比如crash一定次数就启用轻量版本或者自动重新启动等等,下面我们来动手搞一个这样的装置!这也是我第三个s开头的开源库Signal


注意:前方高能!阅读本文最好有一点ndk开发的知识噢!没有也没关系,冲吧!


Native Crash


native crash不同于java/kotlin层的crash,在java环境中,如果程序出现了不可预期的crash(即没有捕获),就会往上抛出给最终的线程uncaghtexceptionhandler,在这里我们可以再次处理,比如屏蔽某个exception即可保持app的稳定,然后native层的crash不一样,native 层的crash大多数是“不可恢复”的,比如某个内存方面的错误,这些往往是不可处理的,需要中断当前进程,所以如果发生了native crash,我们转移到自定义的安全处理,比如自动重启后提示用户等等,就会提高用户很大的体验感(比起闪退)


信号量机制


当native 层发生异常的时候,往往是通过信号的方式发送,给相对应的信号处理器处理


image.png
我们可以从signal.h看到,大概已经定义的信号量有


/**
* #define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
#define SIGABRT 6
#define SIGIOT 6
#define SIGBUS 7
#define SIGFPE 8
#define SIGKILL 9
#define SIGUSR1 10
#define SIGSEGV 11
#define SIGUSR2 12
## define SIGPIPE 13
#define SIGALRM 14
#define SIGTERM 15
#define SIGSTKFLT 16
#define SIGCHLD 17
#define SIGCONT 18
#define SIGSTOP 19
#define SIGTSTP 20
#define SIGTTIN 21
#define SIGTTOU 22
#define SIGURG 23
#define SIGXCPU 24
#define SIGXFSZ 25
#define SIGVTALRM 26
#define SIGPROF 27
#define SIGWINCH 28
#define SIGIO 29
#define SIGPOLL SIGIO
#define SIGPWR 30
#define SIGSYS 31
*/

具体的含义可自定百度或者google,相信如果开发者都能在bugly等bug平台上看到


信号量处理函数sigaction


一般的我们有很多种方式定义信号量处理函数,这里介绍sigaction
头文件:#include<signal.h>


定义函数:int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact)


函数说明:sigaction会依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。如参数结构sigaction定义如下


struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};

信号处理函数可以采用void (*sa_handler)(int)或void (*sa_sigaction)(int, siginfo_t *, void *)。到底采用哪个要看sa_flags中是否设置了SA_SIGINFO位,如果设置了就采用void (*sa_sigaction)(int, siginfo_t *, void *),此时可以向处理函数发送附加信息;默认情况下采用void (*sa_handler)(int),此时只能向处理函数发送信号的数值。


sa_handler:此参数和signal()的参数handler相同,代表新的信号处理函数,其他意义请参考signal();
sa_mask:用来设置在处理该信号时暂时将sa_mask指定的信号集搁置;
sa_restorer:此参数没有使用;
sa_flags :用来设置信号处理的其他相关操作,下列的数值可用。sa_flags还可以设置其他标志:
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号。参考


即我们可以通过这个函数,注册我们想要的信号处理,如果当SIGABRT信号到来时,我们希望将其引到自我们自定义信号处理,即可采用以下方式


 sigaction(SIGABRT, &sigc, nullptr);

其中sigc为sigaction结构体的变量


struct sigaction sigc;
//sigc.sa_handler = SigFunc;
sigc.sa_sigaction = SigFunc;
sigemptyset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO;

SigFunc为我们定义处理函数的指针,我们可以设定这样一个函数,去处理我们想要拦截的信号


void SigFunc(int sig_num, siginfo *info, void *ptr) {
自定义处理
}

native crash拦截


有了前面这些基础知识,我们就开始封装我们的crash拦截吧,作为库开发者,我们希望把拦截的信号量交给上层去处理,所以我们的层次是这样的


image.png
所以我们可以有以下代码,具体细节可以看Signal
我们给出函数处理器


jobject currentObj;
JNIEnv *currentEnv = nullptr;

void SigFunc(int sig_num, siginfo *info, void *ptr) {
// 这里判空并不代表这个对象就是安全的,因为有可能是脏内存

if (currentEnv == nullptr || currentObj == nullptr) {
return;
}
__android_log_print(ANDROID_LOG_INFO, TAG, "%d catch", sig_num);
__android_log_print(ANDROID_LOG_INFO, TAG, "crash info pid:%d ", info->si_pid);
jclass main = currentEnv->FindClass("com/example/lib_signal/SignalController");
jmethodID id = currentEnv->GetMethodID(main, "callNativeException", "(I)V");
if (!id) {
return;
}
currentEnv->CallVoidMethod(currentObj, id, sig_num);
currentEnv->DeleteGlobalRef(currentObj);


}

当so库被加载的时候由系统自动调用JNI_OnLoad
extern "C" jint JNI_OnLoad(JavaVM *vm, void *reserved) {
jint result = -1;
// 直接用vm进行赋值,不然不可靠
if (vm->GetEnv((void **) &currentEnv, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
return JNI_VERSION_1_4;
}

其中currentEnv代表着当前jni环境,我们在JNI_OnLoad阶段进行初始化即可,currentObj即代表我们要调用的方法对象,因为我们要回调到java层,所以native肯定需要一个java对象,具体可以看到Signal里面的处理,值得注意的是,我们在native想要在其他函数使用java对象的话,在初始函数赋值的时候,就必须采用env->NewGlobalRef方式分配一个全局变量,不然在该函数结束的时候,对象的内存就会变成脏变量(注意不是NULL)。


Spi机制的运用


如果还不明白spi机制的话,可以查看我之前写的这篇spi机制,因为我们最终会将信号信息传递给java层,所以最终会在java最后执行我们的重启处理,但是重启前我们可能会使用各种自定义的处理方案,比如弹出toast或者各种自定义操作,那么这种自定义的处理就很合适用spi接口暴露给具体的使用者即可,所以我们Signal定义了一个接口


interface CallOnCatchSignal {
fun onCatchSignal(signal: Int,context: Context)
}

外部库的调用者实现这个接口,将实现类配置在META-INF.services目录即可,如图


image.png
如此一来,我们就可以在自定义的MyHandler实现自己的重启逻辑,比如重启/自定义上报crash等等,demo可以看Signal的处理


ANR


关于anr也是一个很有趣的话题,我们可以看到anr也会导致闪退,主要是国内各个厂商都有自己的自定义化处理,比如常规的弹出anr框或者主动闪退,无论是哪一种,对于用户来说都不是一个好的体验。


ANR传递过程


以android 11为例子,最终anr被检测发生后,会调用ProcessErrorStateRecord类的appNotResponding方法,去进行dump 墓碑文件的操作,这个时候就会调用发送一个信号为Signal_Quit的信号,对应的常量为3,所以如果我们想检测到anr后去进行自定义处理的话,按照上面所说直接用sigaction可以吗?


image.png


然而如果直接用sigaction去注册Signal_Quit信号进行处理的话,会发现居然什么都没有回调!那么这发生了什么!


原因就是我们进程继承Zygote进行的时候就把主线程信号的掩码也继承了,Zygote进程把这三个信号量加入了掩码,该方法被调用在init方法中


image.png
掩码的作用就是使得当前的线程不相应这三个信号量,交给其他线程处理


那么其他线程这里指的是什么?其实就是SignalCatcher线程,通常我们发生anr的时候也能看到log输出,最终在run方法注册处理函数


image.png
最终调用WaitForSignal


image.png
调用wait方法


image.png
这个sigwait方法也是一个注册信号处理函数的方法,跟sigaction的区别可参考


取消block


经过上面的分析,相信能了解到为什么Signal_Quit监听不了了,我们也知道,zygote通过掩码把信号进行了屏蔽,那么我们有办法把这个屏蔽给打开吗?答案是有的


pthread_sigmask(SIG_UNBLOCK, &mask, &old))

sigemptyset(&mask);
sigaddset(&mask, SIGQUIT);

我们可以通过pthread_sigmask设置为非block,即参数1的标志,把要取消屏蔽的信号放入即可,如图就是把SIGQUIT取消了,这样一来我们再使用sigaction去注册SIGQUIT就可以在信号出发时执行我们的anr处理逻辑了。值得注意的是,SIGQUIT触发也不一定由anr发生,这是一个必要但不充分的条件,所以我们还要添加其他的判断,比如我们可以判断一个queue里面的当前message的when参数来判断这个消息在队列待了多久,又或者是我们自定义一个异步消息去查看这个消息什么时候回调了handler等等方法,最终判断是否是anr,当然这个不是百分百准确,目前我也没想到百分百准确的方法,因为FileObserve监听traces文件已经在android5以上不能用了,所以Signal里面没有给出具体的判断,只给了一个参考例子。


最后


上述所讲的都在Signal这个库里面有源码与注释,用起来吧!自定义处理可以用作检测crash,anr,也可以用作一个安全装置,发生crash重启等等,只要有脑洞,都可以实现!最后记得点个赞啦!


作者:Pika
链接:https://juejin.cn/post/7114181318644072479
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册