黑科技!讓Native Crash 與ANR無處發(fā)泄!

1.前言

高產(chǎn)似母豬的我,又帶來了干貨記錄,本次是對signal的一個總結與回顧。不知道你們開發(fā)中,是否會遇到小部分的nativecrash 或者 anr,這部分往往是由第三方庫導致的或者當前版本沒辦法修復的bug導致的,往往這些難啃的crash,對現(xiàn)有的crash數(shù)據(jù)指標造成一定影響,同時也對這小部分crash用戶不友好,那么我們有沒有辦法實現(xiàn)一套crash or anr重啟機制呢?其實是有的,相信在各個大廠都有一套“安全氣囊”裝置,比如crash一定次數(shù)就啟用輕量版本或者自動重新啟動等等,下面我們來動手搞一個這樣的裝置!這也是我第三個s開頭的開源庫Signal。

https://github.com/TestPlanB/Signal

注意:前方高能!閱讀本文最好有一點ndk開發(fā)的知識噢!沒有也沒關系,沖吧!

2.Native Crash

native crash不同于java/kotlin層的crash,在java環(huán)境中,如果程序出現(xiàn)了不可預期的crash(即沒有捕獲),就會往上拋出給最終的線程uncaghtexceptionhandler,在這里我們可以再次處理,比如屏蔽某個exception即可保持app的穩(wěn)定,然后native層的crash不一樣,native 層的crash大多數(shù)是“不可恢復”的,比如某個內(nèi)存方面的錯誤,這些往往是不可處理的,需要中斷當前進程,所以如果發(fā)生了native crash,我們轉移到自定義的安全處理,比如自動重啟后提示用戶等等,就會提高用戶很大的體驗感(比起閃退)。

信號量機制

當native 層發(fā)生異常的時候,往往是通過信號的方式發(fā)送,給相對應的信號處理器處理。

截屏2022-08-05 16.12.41.png

我們可以從signal.h看到,大概已經(jīng)定義的信號量有:

/**
 * #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,相信如果開發(fā)者都能在bugly等bug平臺上看到。

信號量處理函數(shù)sigaction

一般的我們有很多種方式定義信號量處理函數(shù),這里介紹sigaction 頭文件:#include<signal.h>

定義函數(shù):int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact)

函數(shù)說明:sigaction會依參數(shù)signum指定的信號編號來設置該信號的處理函數(shù)。參數(shù)signum可以指定SIGKILL和SIGSTOP以外的所有信號。如參數(shù)結構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);
};

信號處理函數(shù)可以采用void (sa_handler)(int)或void (sa_sigaction)(int, siginfo_t *, void )。到底采用哪個要看sa_flags中是否設置了SA_SIGINFO位,如果設置了就采用void (sa_sigaction)(int, siginfo_t *, void ),此時可以向處理函數(shù)發(fā)送附加信息;默認情況下采用void (sa_handler)(int),此時只能向處理函數(shù)發(fā)送信號的數(shù)值。

sa_handler:此參數(shù)和signal()的參數(shù)handler相同,代表新的信號處理函數(shù),其他意義請參考signal();

sa_mask:用來設置在處理該信號時暫時將sa_mask指定的信號集擱置;

sa_restorer:此參數(shù)沒有使用;

**sa_flags **:用來設置信號處理的其他相關操作,下列的數(shù)值可用。

sa_flags還可以設置其他標志:SA_RESETHAND:當調(diào)用信號處理函數(shù)時,將信號的處理函數(shù)重置為缺省值SIG_DFL SA_RESTART:如果信號中斷了進程的某個系統(tǒng)調(diào)用,則系統(tǒng)自動啟動該系統(tǒng)調(diào)用 SA_NODEFER :一般情況下, 當信號處理函數(shù)運行時,內(nèi)核將阻塞該給定信號。但是如果設置SA_NODEFER標記, 那么在該信號處理函數(shù)運行時,內(nèi)核將不會阻塞該信號。參考

https://blog.csdn.net/qq_32198115/article/details/120720820

即我們可以通過這個函數(shù),注冊我們想要的信號處理,如果當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為我們定義處理函數(shù)的指針,我們可以設定這樣一個函數(shù),去處理我們想要攔截的信號。

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

native crash攔截

有了前面這些基礎知識,我們就開始封裝我們的crash攔截吧,作為庫開發(fā)者,我們希望把攔截的信號量交給上層去處理,所以我們的層次是這樣的。

截屏2022-08-05 16.16.31.png

所以我們可以有以下代碼,具體細節(jié)可以看Signal 我們給出函數(shù)處理器。

https://github.com/TestPlanB/Signal

jobject currentObj;
JNIEnv *currentEnv = nullptr;
void SigFunc(int sig_num, siginfo *info, void *ptr) {
    // 這里判空并不代表這個對象就是安全的,因為有可能是臟內(nèi)存

    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);


}

被加載的時候由系統(tǒng)自動調(diào)用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環(huán)境,我們在JNI_OnLoad階段進行初始化即可,currentObj即代表我們要調(diào)用的方法對象,因為我們要回調(diào)到java層,所以native肯定需要一個java對象,具體可以看到Signal里面的處理,值得注意的是,我們在native想要在其他函數(shù)使用java對象的話,在初始函數(shù)賦值的時候,就必須采用env->NewGlobalRef方式分配一個全局變量,不然在該函數(shù)結束的時候,對象的內(nèi)存就會變成臟變量(注意不是NULL)。

Spi機制的運用

如果還不明白spi機制的話,可以查看我之前寫的這篇spi機制,因為我們最終會將信號信息傳遞給java層,所以最終會在java最后執(zhí)行我們的重啟處理,但是重啟前我們可能會使用各種自定義的處理方案,比如彈出toast或者各種自定義操作,那么這種自定義的處理就很合適用spi接口暴露給具體的使用者即可,所以我們Signal定義了一個接口。

https://juejin.cn/post/7109666495566708766

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

外部庫的調(diào)用者實現(xiàn)這個接口,將實現(xiàn)類配置在META-INF.services目錄即可,如圖:

截屏2022-08-05 16.19.34.png

如此一來,我們就可以在自定義的MyHandler實現(xiàn)自己的重啟邏輯,比如重啟/自定義上報crash等等,demo可以看Signal的處理。

3.ANR

關于anr也是一個很有趣的話題,我們可以看到anr也會導致閃退,主要是國內(nèi)各個廠商都有自己的自定義化處理,比如常規(guī)的彈出anr框或者主動閃退,無論是哪一種,對于用戶來說都不是一個好的體驗。

ANR傳遞過程

以android 11為例子,最終anr被檢測發(fā)生后,會調(diào)用ProcessErrorStateRecord類的appNotResponding方法,去進行dump 墓碑文件的操作,這個時候就會調(diào)用發(fā)送一個信號為Signal_Quit的信號,對應的常量為3,所以如果我們想檢測到anr后去進行自定義處理的話,按照上面所說直接用sigaction可以嗎?

截屏2022-08-05 16.20.18.png

然而如果直接用sigaction去注冊Signal_Quit信號進行處理的話,會發(fā)現(xiàn)居然什么都沒有回調(diào)!那么這發(fā)生了什么!

原因就是我們進程繼承Zygote進行的時候就把主線程信號的掩碼也繼承了,Zygote進程把這三個信號量加入了掩碼,該方法被調(diào)用在init方法中。

截屏2022-08-05 16.20.59.png

掩碼的作用就是使得當前的線程不響應這三個信號量,交給其他線程處理。

那么其他線程這里指的是什么?其實就是SignalCatcher線程,通常我們發(fā)生anr的時候也能看到log輸出,最終在run方法注冊處理函數(shù)。

截屏2022-08-05 16.21.31.png

最終調(diào)用WaitForSignal:

截屏2022-08-05 16.22.06.png

調(diào)用wait方法:

截屏2022-08-05 16.22.13.png

這個sigwait方法也是一個注冊信號處理函數(shù)的方法,跟sigaction的區(qū)別可參考。

https://blog.csdn.net/finuxz/article/details/54969582

取消block

經(jīng)過上面的分析,相信能了解到為什么Signal_Quit監(jiān)聽不了了,我們也知道,zygote通過掩碼把信號進行了屏蔽,那么我們有辦法把這個屏蔽給打開嗎?答案是有的。

pthread_sigmask(SIG_UNBLOCK, &mask, &old))
sigemptyset(&mask);sigaddset(&mask, SIGQUIT);

我們可以通過pthread_sigmask設置為非block,即參數(shù)1的標志,把要取消屏蔽的信號放入即可,如圖就是把SIGQUIT取消了,這樣一來我們再使用sigaction去注冊SIGQUIT就可以在信號出發(fā)時執(zhí)行我們的anr處理邏輯了。值得注意的是,SIGQUIT觸發(fā)也不一定由anr發(fā)生,這是一個必要但不充分的條件,所以我們還要添加其他的判斷,比如我們可以判斷一個queue里面的當前message的when參數(shù)來判斷這個消息在隊列待了多久,又或者是我們自定義一個異步消息去查看這個消息什么時候回調(diào)了handler等等方法,最終判斷是否是anr,當然這個不是百分百準確,目前我也沒想到百分百準確的方法,因為FileObserve監(jiān)聽traces文件已經(jīng)在android5以上不能用了,所以Signal里面沒有給出具體的判斷,只給了一個參考例子。

4最后

上述所講的都在Signal這個庫里面有源碼與注釋,用起來吧!自定義處理可以用作檢測crash,anr,也可以用作一個安全裝置,發(fā)生crash重啟等等,只要有腦洞,都可以實現(xiàn)!最后記得點個贊啦!

文章引用

https://blog.csdn.net/qq_32198115/article/details/120720820

Android Native Crash 收集

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容