Android開發(fā)中Signal背后的bug與解決
背景
熟悉我的老朋友可能都知道,之前為了應(yīng)對(duì)crash與anr,開源過一個(gè)“民間偏方”的庫(kù)Signal,用于解決在發(fā)生crash或者anr時(shí)進(jìn)行應(yīng)用的重啟,從而最大程度減少其壞影響。
在維護(hù)的過程中,發(fā)生過這樣一件趣事,就是有位朋友發(fā)現(xiàn)在遇到信號(hào)為SIGSEGV時(shí),再調(diào)用信號(hào)處理函數(shù)的時(shí)候
void SigFunc(int sig_num, siginfo *info, void *ptr) { // 這里判空并不代表這個(gè)對(duì)象就是安全的,因?yàn)橛锌赡苁桥K內(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", "(ILjava/lang/String;)V"); if (!id) { __android_log_print(ANDROID_LOG_INFO, TAG, "%d !id!id!id!id!id!id!id", sig_num); return; } __android_log_print(ANDROID_LOG_INFO, TAG, "%d 11111111111111111111", sig_num); jstring nativeStackTrace = currentEnv->NewStringUTF(backtraceToLogcat().c_str()); __android_log_print(ANDROID_LOG_INFO, TAG, "%d 22222222222222222222", sig_num); currentEnv->CallVoidMethod(currentObj, id, sig_num, nativeStackTrace); __android_log_print(ANDROID_LOG_INFO, TAG, "%d 33333333333333333333", sig_num); // 釋放資源 currentEnv->DeleteGlobalRef(currentObj); currentEnv->DeleteLocalRef(nativeStackTrace); }
會(huì)遇到
從上文打印中看到,SIGSEGV被拋出,之后被我們的信號(hào)處理函數(shù)抓到了,但是卻沒有被回調(diào)到j(luò)ava層,反而變成了SIGABRT。還有就是SIGSEGV被捕獲后,卻無法通過jni回調(diào)給java層的重啟處理。本文將從這個(gè)例子出發(fā),從踩坑的過程中去學(xué)習(xí)更多jni知識(shí)。
出現(xiàn)SIGABRT的原因
首先呢,currentEnv是一個(gè)全局的變量,我們一般jni開發(fā)的時(shí)候,都習(xí)慣于保存一個(gè)JNIEnv全局的引用,用于后續(xù)的調(diào)用處理!但是!這樣其實(shí)是一個(gè)風(fēng)險(xiǎn)的操作,比如我們?cè)趕igaction注冊(cè)一個(gè)信號(hào)處理函數(shù)的時(shí)候,那么當(dāng)信號(hào)來的時(shí)候,我們的信號(hào)處理運(yùn)行在哪個(gè)線程呢?
答案是:不確定。當(dāng)信號(hào)處理時(shí),會(huì)根據(jù)當(dāng)前內(nèi)核的調(diào)度,可能會(huì)在當(dāng)前發(fā)出信號(hào)的線程中進(jìn)行處理,同時(shí)也可能會(huì)另外開出一個(gè)線程進(jìn)行處理。而我們的JNIEnv,它其實(shí)是一個(gè)線程相關(guān)的資源,或者說是線程本地資源(TLS),如果我們?cè)谄渌€程中調(diào)用到這個(gè)JNIEnv,那么會(huì)怎么樣呢?比如上面例子中的currentEnv,創(chuàng)建在我們的java層的main線程,此時(shí)在信號(hào)處理函數(shù)中調(diào)用currentEnv->FindClass,那么不好意思,這個(gè)可不屬于當(dāng)前線程的資源,因此linux內(nèi)核就會(huì)發(fā)出一個(gè)SIGABRT信號(hào),提示著這個(gè)操作將被阻斷
java_vm_ext.cc void JavaVMExt::JniAbort(const char* jni_function_name, const char* msg) { Thread* self = Thread::Current(); ScopedObjectAccess soa(self); ArtMethod* current_method = self->GetCurrentMethod(nullptr); std::ostringstream os; os << "JNI DETECTED ERROR IN APPLICATION: " << msg; if (jni_function_name != nullptr) { os << "\n in call to " << jni_function_name; } // TODO: is this useful given that we're about to dump the calling thread's stack? if (current_method != nullptr) { os << "\n from " << current_method->PrettyMethod(); } if (check_jni_abort_hook_ != nullptr) { check_jni_abort_hook_(check_jni_abort_hook_data_, os.str()); } else { // Ensure that we get a native stack trace for this thread. ScopedThreadSuspension sts(self, ThreadState::kNative); LOG(FATAL) << os.str(); UNREACHABLE(); } }
JniAbort 調(diào)用會(huì)在所有的方法調(diào)用前進(jìn)行檢測(cè),如果使用到了其他線程的JNIEnv,就會(huì)發(fā)出SIGABRT信號(hào)并打印堆棧信息,用于排查
java_vm_ext.cc:578] JNI DETECTED ERROR IN APPLICATION: thread Thread[3,tid=22651,Native,Thread*=0xb400007c96340270,peer=0x12c4d1a0,"Thread-3"] using JNIEnv* from thread Thread[1,tid=22160,Runnable,Thread*=0xb400007c9630dbe0,peer=0x73467b00,"main"]
那么如果我們真的有場(chǎng)景需要通過在信號(hào)處理函數(shù)中調(diào)用到JNIEnv怎么辦,其實(shí)也很簡(jiǎn)單,通過javaVm重新獲取一個(gè)JNIEnv即可,javaVm保證是虛擬機(jī)中唯一的,因此可以放在全局變量中,當(dāng)我們想要在信號(hào)處理函數(shù)時(shí)調(diào)用到j(luò)ni方法,可重新獲取當(dāng)前線程的環(huán)境
信號(hào)處理函數(shù)中
if (javaVm->GetEnv((void **) ¤tEnv, JNI_VERSION_1_4) != JNI_OK) { return ; }
SIGSEGV被捕獲但是調(diào)用jni無法進(jìn)行
我們的例子是這樣的,在java層調(diào)用一個(gè)jni函數(shù),這個(gè)函數(shù)通過raise調(diào)用向自身發(fā)送一個(gè)SIGSEGV信號(hào)
raise(SIGSEGV);
此時(shí)我們的信號(hào)處理函數(shù)能夠捕獲到這個(gè)事件,但是通過currentEnv->CallVoidMethod卻無法調(diào)用相應(yīng)的java層方法了,同時(shí)log中出現(xiàn)一個(gè)StackOverflowError
Process: com.example.signal, PID: 24575 java.lang.StackOverflowError: stack size 8192KB at com.example.signal.MainActivity.throwNativeCrash(Native Method) at com.example.signal.MainActivity.onCreate$lambda-0(MainActivity.kt:23) at com.example.signal.MainActivity.$r8$lambda$__atZomnwlT46HKNaZgatRAAqwU(Unknown Source:0) at com.example.signal.MainActivity$$ExternalSyntheticLambda0.onClick(Unknown Source:2) at android.view.View.performClick(View.java:8160)
那么這個(gè)究竟是怎么一回事呢?
首先我們要明白,我們真的是因?yàn)闂?nèi)存耗盡了出現(xiàn)StackOverflowError了嗎?當(dāng)然不是!我們只是在jni向自己線程發(fā)出了一個(gè)SIGSEGV信號(hào)罷了,怎么跟棧溢出扯上關(guān)系了?我們從art虛擬機(jī)開始說起
在art虛擬機(jī)中,出現(xiàn)SIGSEGV時(shí),會(huì)默認(rèn)先回調(diào)這個(gè)方法
# fault_handler.cc // Signal handler called on SIGSEGV. static bool art_fault_handler(int sig, siginfo_t* info, void* context) { return fault_manager.HandleFault(sig, info, context); }
核心是方法
bool FaultManager::HandleFault(int sig, siginfo_t* info, void* context) { if (VLOG_IS_ON(signals)) { PrintSignalInfo(VLOG_STREAM(signals) << "Handling fault:" << "\n", info); } #ifdef TEST_NESTED_SIGNAL // Simulate a crash in a handler. raise(SIGSEGV); #endif 針對(duì)生成機(jī)器碼處理 if (IsInGeneratedCode(info, context, true)) { VLOG(signals) << "in generated code, looking for handler"; for (const auto& handler : generated_code_handlers_) { VLOG(signals) << "invoking Action on handler " << handler; if (handler->Action(sig, info, context)) { // We have handled a signal so it's time to return from the // signal handler to the appropriate place. return true; } } } // We hit a signal we didn't handle. This might be something for which // we can give more information about so call all registered handlers to // see if it is. 其他非機(jī)器碼處理 if (HandleFaultByOtherHandlers(sig, info, context)) { return true; } // Set a breakpoint in this function to catch unhandled signals. 只是打印了一些log art_sigsegv_fault(); return false; }
我們可以留意到,在上面有這么一個(gè)判斷IsInGeneratedCode,如果是則嘗試遍歷generated_code_handlers_里面的handler對(duì)信號(hào)處理,那么IsInGeneratedCode是個(gè)啥?其實(shí)它是指dex字節(jié)碼編譯成機(jī)器碼這些代碼,art虛擬會(huì)在編譯成機(jī)器碼的時(shí)候,生成一些虛擬機(jī)相關(guān)的指令,因此如果SIGSEGV是在這些機(jī)器碼中生成的,那么就要通過generated_code_handlers_里面的處理器去處理,同時(shí)如果是非機(jī)器碼生成的,則走到HandleFaultByOtherHandlers方法中進(jìn)行處理
bool FaultManager::HandleFaultByOtherHandlers(int sig, siginfo_t* info, void* context) { if (other_handlers_.empty()) { return false; } Thread* self = Thread::Current(); DCHECK(self != nullptr); DCHECK(Runtime::Current() != nullptr); DCHECK(Runtime::Current()->IsStarted()); for (const auto& handler : other_handlers_) { if (handler->Action(sig, info, context)) { return true; } } return false; }
因此我們特別關(guān)注一下generated_code_handlers_,other_handlers_(針對(duì)默認(rèn)處理),它們都是一個(gè)集合std::vector<FaultHandler*> 我們看到它的添加元素方法,在FaultManager::AddHandler中
void FaultManager::AddHandler(FaultHandler* handler, bool generated_code) { DCHECK(initialized_); if (generated_code) { generated_code_handlers_.push_back(handler); } else { other_handlers_.push_back(handler); } }
這里面添加的handler都是FaultHandler的子類,分別是NullPointerHandler,SuspensionHandler,StackOverflowHandler,JavaStackTraceHandler
雖然JavaStackTraceHandler被加入到了other_handlers_,但是依舊會(huì)判斷是否處于虛擬機(jī)code中
在這里我們明白了SIGSEGV虛擬機(jī)的默認(rèn)處理,一般SIGSEGV都會(huì)進(jìn)入上述handler的判斷,如果滿足了條件就會(huì)先執(zhí)行(之后才執(zhí)行到我們的信號(hào)處理函數(shù),如果系統(tǒng)棧溢出,那么有可能執(zhí)行不到自己的信號(hào)處理器)。本例子中raise(SIGSEGV)向自己的線程拋出了SIGSEGV,如果信號(hào)處理器中沒有采用Call系列調(diào)用到j(luò)ava層的話,那也不會(huì)有問題。
如果調(diào)用到了java層,那么就以棧溢出的形式打印log并重新發(fā)一個(gè)信號(hào)值為SIGKILL的信號(hào)殺死當(dāng)前進(jìn)程。(這里一直有個(gè)疑惑點(diǎn),目前還沒在art源碼上看到為什么會(huì)這樣,如果有知道的大佬可勞煩告知)
Sending signal. PID: 29066 SIG: 9
解決方法也比較簡(jiǎn)單,當(dāng)我們異常處理器無法在棧異常情況下,我們可以事先采用sigaltstack分配一塊棧空間
stack_t ss; if(NULL == (ss.ss_sp = calloc(1, SIGNAL_CRASH_STACK_SIZE))){ Handle_Exception(); break; } ss.ss_size = SIGNAL_CRASH_STACK_SIZE; ss.ss_flags = 0; if(0 != sigaltstack(&ss, NULL)) { Handle_Exception(); break; }
同時(shí)設(shè)置flag為SA_ONSTACK即可,讓信號(hào)處理函數(shù)有一個(gè)安全的棧空間,得以進(jìn)行后續(xù)調(diào)用
sigc.sa_flags = SA_SIGINFO|SA_ONSTACK;
小結(jié)
本次算是一個(gè)記錄,以一個(gè)現(xiàn)象例子,更深入了解jni調(diào)用,希望讀者有所收獲,最后繼續(xù)貼一下項(xiàng)目地址,如果有更多好點(diǎn)子的話,請(qǐng)多多pr!
以上就是Android開發(fā)中Signal背后的bug與解決的詳細(xì)內(nèi)容,更多關(guān)于Android Signal bug解決的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
android 通知Notification詳解及實(shí)例代碼
這篇文章主要介紹了android 通知Notification詳解及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2016-12-12Android 在viewPager中雙指縮放圖片雙擊縮放圖片單指拖拽圖片的實(shí)現(xiàn)思路
本文通過實(shí)例代碼給大家講解了Android 在viewPager中雙指縮放圖片雙擊縮放圖片單指拖拽圖片的實(shí)現(xiàn)思路及解決方案,需要的朋友參考下吧2017-05-05解決Android Studio 3.0 butterknife:7.0.1配置的問題
下面小編就為大家分享一篇解決Android Studio 3.0 butterknife:7.0.1配置的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2017-12-12Android工具欄頂出轉(zhuǎn)場(chǎng)動(dòng)畫的實(shí)現(xiàn)方法實(shí)例
這篇文章主要給大家介紹了關(guān)于Android工具欄頂出轉(zhuǎn)場(chǎng)動(dòng)畫的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09android自定義view實(shí)現(xiàn)圓周運(yùn)動(dòng)
這篇文章主要為大家詳細(xì)介紹了android自定義view實(shí)現(xiàn)逆時(shí)針和順時(shí)針轉(zhuǎn)動(dòng)的圓周運(yùn)動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-03-03Android ListView中動(dòng)態(tài)顯示和隱藏Header&Footer的方法
這篇文章主要介紹了Android ListView中動(dòng)態(tài)顯示和隱藏Header&Footer的方法及footer的兩種正確使用方法,本文介紹的非常詳細(xì),具有參考借鑒價(jià)值,對(duì)listview header footer相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2016-08-08Android編程之計(jì)時(shí)器Chronometer簡(jiǎn)單示例
這篇文章主要介紹了Android計(jì)時(shí)器Chronometer簡(jiǎn)單用法,結(jié)合實(shí)例形式分析了Android計(jì)時(shí)器Chronometer的定義、事件響應(yīng)及界面布局相關(guān)操作技巧,需要的朋友可以參考下2017-08-08Android 阿里云OSS文件上傳的實(shí)現(xiàn)示例
這篇文章主要介紹了Android 阿里云OSS文件上傳的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08Android開發(fā)進(jìn)階自定義控件之滑動(dòng)開關(guān)實(shí)現(xiàn)方法【附demo源碼下載】
這篇文章主要介紹了Android開發(fā)進(jìn)階自定義控件之滑動(dòng)開關(guān)實(shí)現(xiàn)方法,結(jié)合實(shí)例形式詳細(xì)分析了Android自定義開關(guān)控件的原理、實(shí)現(xiàn)步驟與相關(guān)操作技巧,需要的朋友可以參考下2016-08-08