精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

RN與hawk碰撞的火花之C++異常捕獲

開發 前端
為什么要了解unwind,因為三次 crash 都和 __gxx_personality_v0 息息相關,而 __gxx_personality_v0 正是unwind 進行棧展開的關鍵函數。只有弄懂了unwind原理,才有可能找到問題的根本原因。

一、序言

二、unwind原理

    1. fbjni異常拋出

    2. C++ 標準庫與unwind的關系

    3. C++ try catch實現

    4. crash 元兇之一

    5. crash 元兇之二

    6. crash 平臺堆棧為什么只有一行

    7. Android 系統的unwind

三、動態庫符號鏈接

    1. dlopen流程

    2. 為什么只有線上包才能復現

四、問題修復

    1. MMKV動態下發加載

    2. 啟動白屏

    3. Android 5.1 閃退

五、結語

一、序言

在需求迭代開發過程中,有測試團隊的小伙伴反饋在 RN 頁面 偶爾會出現 crash,且堆棧只有一行,指向了MMKV,看完之后一頭霧水,為什么 RN 頁面會出現 MMKV的 crash,而且堆棧為什么只有一行,其實這些都和 unwind 有直接和間接的關系。就此問題而言,雖然 crash 平臺的堆棧只有一行,但是通過抓取到的墓碑文件,可以看到此問題和 __gxx_personality_v0 相關。

圖片圖片

自 RN 升級到 0.72.5 版本之后,已經是第三次出現和 __gxx_personality_v0 相關的 crash。

為什么 crash 堆棧只有一行?為什么 RN 的 crash 會指向MMKV?讓我們帶著這些疑問來詳細了解一下unwind。

二、unwind原理

為什么要了解unwind,因為三次 crash 都和 __gxx_personality_v0 息息相關,而 __gxx_personality_v0 正是unwind 進行棧展開的關鍵函數。只有弄懂了unwind原理,才有可能找到問題的根本原因。

說起__gxx_personality_v0,就不得不提起因它而起的第一次 crash。在RN 升級 0.72.5 版本之初,是在RN Demo 中進行的,升級改造完成之后,才移植到得物 App進行測試回歸的。回歸測試過程中發現了幾例奇怪的 crash(如下圖所示),但是在 RN demo 中從未出現過。

圖片圖片

堆棧中都出現了__gxx_personality_v0,通過google了解到此函數是 unwind 回溯堆棧所用到的,為什么 fbjni 會調用到這里,難道是出現了什么異常?后來通過反復自測,發現JS通過NativeModule調用到原生方法,當原生方法調用過程中拋出了異常,就會導致這個crash。

fbjni異常拋出

fbjni 是Facebook所開發的三方庫,旨在簡化和減少使用 JNI 的一些復雜性,使得開發者可以更容易地編寫和維護 JNI 代碼。而 RN 也是通過 fbjni 來實現 Android 側 JNI 方法的綁定,NativeModule 的調用最終也是通過 fbjni 調用過來。正常Java拋的異常是不會中斷 C++ 的代碼流程,但是 fbjni 對 JNI 方法調用做了一層包裝,接下我們來看下 fbjni 是如何處理 Java 側拋出的異常。

圖片圖片

圖片圖片

可以看到,最終 fbjni 檢測到Java 異常之后,會清理掉Java異常,并拋出一個 C++的異常,這里無需擔心拋出異常導致 crash,上層 RN 有try ctach 處理,并把異常交給我們自定義的 ExceptionHandler 進行處理,那這里拋出異常會直接導致 crash 呢?接下來我們來看下 crash 堆棧中的 __gxx_personality_v0。

經過一番 google,__gxx_personality_v0 是一個由GNU C++ 編譯器(GCC)實現的函數,主要用于支持 C++ 異常處理和堆棧展開。它是一個所謂的 personality function ,這種函數負責處理異常的拋出、捕獲和堆棧展開的行為。__gxx_personality_v0 函數是 GCC 提供的一種實現,用于在 C++ 中支持異常處理機制。當 C++ 異常被拋出時,__gxx_personality_v0 被調用來處理當前的調用棧。這包括識別哪些函數應該從堆棧中展開,以便適當地調用異常處理程序。

因為__gxx_personality_v0 是被 libc++_shared.so 動態庫所導出的,因為得物有很多庫都依賴了C++標準庫,最終打包進的libc++_shared.so 是通過得物殼工程中 pickFirst 來決定的,難道是編譯fbjni與使用的libc++與得物中的 libc++_shared.so 不一致導致的。因為 RN 升級后,ndk版本也發生了改變,得物使用的ndk 21,而升級后的 RN 使用的ndk 23,ndk 21 與 ndk 23 所使用 libc++_shared.so 是有所不同的,當時猜測是__gxx_personality_v0 函數中使用的某個對象結構發生了變化,因為對象結構發生變化,代碼編譯時訪問對象屬性時的偏移量也就可能發生變化,從而引發運行時內存訪問越界(這里只是猜測,具體crash 原因后文中會詳細介紹)。

于是,就拉了fbjni的源碼,將 ndk版本降級為 21 重新編譯了fbjni,然后重新發布 RN 組件到得物,果然 ndk 降級為 21 之后,這個問題就不復存在了,拋出的異常能夠被正常catch住。那這樣的話,RN 0.72.5 版本的 hermes 引擎也是通過 ndk 23 編譯的,當執行JS過程中出現異常勢必也會受到影響,果然主動拋出JS異常后,hermes也有相同的問題,不得不把 hermes 源碼也拉下來自行編譯一份。

至此,__gxx_personality_v0引發的 crash 就告一段落,RN 0.72.5 版本也在未延期的情況下正常順利上線了。但時隔多日,crash平臺又出現了相似的崩潰,堆棧仍然報在__gxx_personality_v0,不同點是之前錯誤信號是 SIGSEGV,而這次的信號是SIGABRT,說明是程序自己調用了abort() 函數終止了進程。為了探尋__gxx_personality_v0為何會觸發 abort(),就不得不對__gxx_personality_v0有一個深入的了解,這就要從C++的 try catch 開始講起。

圖片圖片

C++標準庫與unwind的關系

這里介紹三個庫,分別是libc++、libc++abi、libunwind:

libc++

libc++是 C++標準庫的一個實現,提供了對 C++ 標準庫的完整支持,包括如標準容器(如std::vector、std::map)、算法、輸入輸出流等。

特性:

  • 支持 C++11、C++14、C++17 及后續版本的標準。
  • 注重對 C++ 新特性的支持,并且設計上優化了性能和可用性。
  • 提供完整的 STL (標準模板庫)實現。

用途:

開發 C++ 應用程序時,libc++作為標準庫被直接引用,用于標準語言特性的訪問。

libc++abi

libc++abi是與libc++相關的一個庫,主要負責為 C++ 標準庫提供應用二進制接口(ABI)支持,特別是在異常處理和運行時類型識別 (RTTI) 方面。

特性:

  • 實現了 C++ 異常處理中的關鍵功能,如 __cxa_throw 和 __cxa_rethrow。
  • 提供動態類型信息的支持,例如 typeinfo 和 dynamic_cast。

用途:

作為libc++的底層支持庫,為 C++ 應用程序提供必要的 ABI 支持。

libunwind

libunwind是一個用于堆棧展開、異常處理和回調的庫,提供了一種跨平臺的 API 來處理 C 和 C++ 的堆棧展開。

特性:

  • 允許捕獲和恢復程序執行上下文,并支持函數調用棧的展開。
  • 在異常處理和調試過程中,能夠訪問調用棧信息。

用途:

主要在異常處理和調試中使用,幫助實現C++中的堆棧展開。

它們三者之間的關系:

  • libc++ 和 libc++abi :

libc++依賴于libc++abi來執行一些關鍵功能,如處理 C++ 異常的拋出與捕獲。

使用libc++的C++應用程序通常會隱式依賴 libc++abi,因為后者提供了庫的 ABI 方面的支持。

  • libc++abi 和 libunwind :在處理 C++ 異常時,libc++abi可能會使用libunwind 來支持堆棧展開。當一個異常被拋出時,libunwind負責展開調用堆棧,確保所有必要的清理操作得以執行。

C++ try catch實現

根據C++的標準,異常拋出后如果在當前函數內沒有被捕捉(catch),它就要沿著函數的調用鏈繼續往上拋,直到走完整個調用鏈,或者在某個函數中找到相應的catch。如果走完調用鏈都沒有找到相應的catch,那么std::terminate()就會被調用,這個函數默認是把程序 abort,而如果最后找到了相應的catch,就會進入該catch代碼塊,執行相應的操作。

程序中的catch那部分代碼有一個專門的名字叫作:Landing pad。

從拋異常開始到執行landing pad里的代碼這中間的整個過程叫作stack unwind,這個過程包含了兩個階段:

  1. 從拋異常的函數開始,一幀一幀地在每個調用函數里查找landing pad。
  2. 如果沒有找到landing pad則把程序abort,否則,則記下landing pad的位置,再重新回到拋異常的函數那里開始,一幀一幀地清理調用鏈上各個函數內部的局部變量,直到landing pad所在的函數為止。

簡而言之,正常情況下,stack unwind所要做的事情就是從拋出異常的函數開始,找到catch所在的函數,然后從頭開始清理調用鏈上的已經創建了的局部變量。

注:下文介紹到 unwind 實現都是基于GCC版本。

異常拋出

先看一下異常是如何拋出的:

圖片圖片

這是 fbjni中的一段異常拋出代碼,我們來看一下反編譯之后的代碼:

圖片圖片

可以看到,異常拋出時會先使用__cxa_allocate_exception 來分配一個異常對象,然后再調用__cxa_throw將異常拋出,我們再來看一下__cxa_throw 實現。

圖片圖片

可以看到,最終是調用到了_Unwind_RaiseException ,而 _Unwind_RaiseException函數就是用于進行stack unwind的。

棧展開

它在用戶執行throw時被調用,然后從當前函數開始,對調用鏈上每個函數幀都調用一個叫作personality routine的函數(__gxx_personality_v0),該函數由上層的語言定義及提供實現,_Unwind_RaiseException 會在內部把當前函數棧的調用現場重建,然后傳給personality routine,personality routine則主要負責做兩件事情:

  • 檢查當前函數是否含有相應catch可以處理上面拋出的異常。
  • 清掉調用棧上的局部變量。

接下來我們來看一下 _Unwind_RaiseException 的具體實現:

圖片圖片

圖片圖片

可以看到 _Unwind_RaiseException 最終拆成兩個函數,第一個函數主要來完成 phase1,而 phase2 則是放在了 _Unwind_RaiseException_Phase2,不過phase1與 phase2都是通過fs.personality 來進行棧展開的, fs.personality其實就是上文中crash堆棧中的__gxx_personality_v0,fs.personality 的來源則是通過 uw_frame_state_for,它的實現在后文中 FDE 再展開介紹。

所以兩個階段主要分為查找和清理,但是最終的實現都是通過C++標準庫中__gxx_personality_v0,接下來我們就看一下__gxx_personality_v0的具體實現。

圖片圖片

__gxx_personality_v0會通過 actions 參數來區分當前是 查找階段 還是 清理階段,根據不同階段進行不同操作,如果是查找階段,則通過scan_eh_tab 來讀取和解析異常處理表。

unwind的進行需要編譯器生成一定的數據來支持,這些數據保存了與每個可能拋異常的函數相關的信息以供運行時查找,那么,編譯器都保存了哪些信息呢?根據Itanium ABI的定義,主要包括以下三類:

  1. unwind table,這個表記錄了與函數相關的信息,共三個字段:函數的起始地址,函數的結束地址,一個info block指針。
  2. unwind descriptor table,這個列表用于描述函數中需要unwind的區域的相關信息。
  3. 語言相關的數據(language specific data area),用于上層語言內部的處理。

以上數據結構的描述來自Itanium ABI的標準定義,但在具體實現時,這些數據是怎么組織以及放到了哪里則是由編譯器來決定的,對于GCC來說,所有與unwind相關的數據都放到 了.eh_frame及.gcc_except_table這兩個section里面了,而且它的格式與內容和標準的定義稍稍有些不同。

下圖來源于網絡,展示了gcc_except_table及language specific data 的格式:

圖片圖片

FDE 與 CIE

剛才上文中介紹到 scan_eh_tab 是來讀取和解析異常處理表,但是 scan_eh_tab 代碼實現較長,這里就不詳細展開介紹了,我們就著重介紹一下 scan_eh_tab 中使用到的 languageSpecificData 是從何而來。

上文中介紹到_Unwind_RaiseException 函數時有提到 uw_frame_state_for,接下來我們就來介紹一下 uw_frame_state_for的實現。

圖片圖片

可以看到,uw_frame_state_for會先通過_Unwind_Find_FDE 讀取出FDE,再從FDE獲取到CIE,最后再通過CIE中的信息填充fs中的字段。

圖片圖片

圖片圖片

可以看到 languageSpecificData 的地址和上文中提到 fs.personality 其實都是通過 FDE 中 CIE 來讀取到的,那 FDE 和 CIE 又是什么呢?

上文中有介紹到棧展開所需的棧幀信息存放在的 eh_frame 段中。這個結構和調試信息中的 debug_frame 信息是相似的,使用的都是dwarf格式的文件結構,但是兩者有一個重要的區別,debug 結構的 frame 在 strip 之后就不再包含調試信息,而且調試信息默認是不會加載的內存中的,當調試器需要的時候從硬盤上讀取數據。

在eh_frame中包含的是一個一個的 FDE 結構,每個 FDE 結構描述了一個函數堆棧的棧幀信息,包含了最為基本的一個函數的起始地址、長度以及CIE地址。

而CIE則是存儲了數據對齊大小、返回地址寄存器、擴展參數字符串 等等,剛剛提到的languageSpecificData地址與 fs.personality 就是從擴展參數字符串中解析出來的。

我們可以看一下GCC 源碼中 FDE 與 CIE 的數據結構定義(可與后文中提到的 Clang 版本進行對比)。

具體的棧展開流程已經介紹完畢了,接下來,看一下C++ 中 try catch 代碼反編譯之后是怎么樣的。

圖片圖片

這個是 fbjni 中一處簡單的try catch處理,我們看一下它反編譯之后的樣子。

.text:000000000000D594 ; void __fastcall facebook::jni::JniException::populateWhat(const facebook::jni::JniException *this)
.text:000000000000D594 _ZNK8facebook3jni12JniException12populateWhatEv
.text:000000000000D594                                         ; CODE XREF: facebook::jni::JniException::what(void)+38↓p
.text:000000000000D594
.text:000000000000D594 var_A0          = -0xA0
.text:000000000000D594 var_98          = -0x98
.text:000000000000D594 var_90          = -0x90
.text:000000000000D594 var_80          = -0x80
.text:000000000000D594 var_70          = -0x70
.text:000000000000D594 var_60          = -0x60
.text:000000000000D594 var_58          = -0x58
.text:000000000000D594 var_48          = -0x48
.text:000000000000D594 var_40          = -0x40
.text:000000000000D594 var_28          = -0x28
.text:000000000000D594 var_20          = -0x20
.text:000000000000D594 var_10          = -0x10
.text:000000000000D594 var_s0          =  0
.text:000000000000D594
.text:000000000000D594 this = X0                               ; const facebook::jni::JniException *
.text:000000000000D594 ; __unwind { // __gxx_personality_v0
.text:000000000000D594                 SUB             SP, SP, #0x70
.text:000000000000D598                 STP             X22, X21, [SP,#0x60+var_20]
.text:000000000000D59C                 STP             X20, X19, [SP,#0x60+var_10]
.text:000000000000D5A0                 STP             X29, X30, [SP,#0x60+var_s0]
.text:000000000000D5A4                 ADD             X29, SP, #0x60
.text:000000000000D5A8                 MRS             X20, #3, c13, c0, #2
.text:000000000000D5AC                 LDR             X8, [X20,#0x28]
.text:000000000000D5B0                 MOV             X19, this
.text:000000000000D5B4 this = X19                              ; const facebook::jni::JniException *
.text:000000000000D5B4                 STUR            X8, [X29,#var_28]
.text:000000000000D5B8 ;   try {
.text:000000000000D5B8                 ADD             X0, SP, #0x60+var_40 ; this
.text:000000000000D5BC                 BL              ._ZN8facebook3jni11ThreadScopeC2Ev ; facebook::jni::ThreadScope::ThreadScope(void)
.text:000000000000D5BC ;   } // starts at D5B8
.text:000000000000D5C0                 ADD             X0, this, #8
.text:000000000000D5C4 ;   try {
.text:000000000000D5C4                 ADD             X8, SP, #0x60+var_58
.text:000000000000D5C8                 BL              _ZNK8facebook3jni7JObject8toStringEv ; facebook::jni::JObject::toString(void)
.text:000000000000D5C8 ;   } // starts at D5C4
.text:000000000000D5CC                 MOV             X21, this
.text:000000000000D5D0                 LDRB            W8, [X21,#0x10]!
.text:000000000000D5D4                 TBNZ            W8, #0, loc_D5E0
.text:000000000000D5D8                 STRH            WZR, [X21]
.text:000000000000D5DC                 B               loc_D600
.text:000000000000D5E0 ; ---------------------------------------------------------------------------
.text:000000000000D5E0
.text:000000000000D5E0 loc_D5E0                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+40↑j
.text:000000000000D5E0                 LDR             X8, [this,#0x20]
.text:000000000000D5E4                 STRB            WZR, [X8]
.text:000000000000D5E8                 LDRB            W8, [this,#0x10]
.text:000000000000D5EC                 STR             XZR, [this,#0x18]
.text:000000000000D5F0                 TBZ             W8, #0, loc_D600
.text:000000000000D5F4                 LDR             X0, [this,#0x20] ; void *
.text:000000000000D5F8                 BL              ._ZdlPv ; operator delete(void *)
.text:000000000000D5FC                 STR             XZR, [this,#0x10]
.text:000000000000D600
.text:000000000000D600 loc_D600                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+48↑j
.text:000000000000D600                                         ; facebook::jni::JniException::populateWhat(void)+5C↑j
.text:000000000000D600                 LDR             X8, [SP,#0x60+var_48]
.text:000000000000D604                 LDUR            Q0, [SP,#0x60+var_58]
.text:000000000000D608                 MOV             W9, #1
.text:000000000000D60C                 ADD             X0, SP, #0x60+var_40 ; this
.text:000000000000D610                 STR             X8, [X21,#0x10]
.text:000000000000D614                 STR             Q0, [X21]
.text:000000000000D618                 STRB            W9, [this,#0x28]
.text:000000000000D61C                 BL              ._ZN8facebook3jni11ThreadScopeD2Ev ; facebook::jni::ThreadScope::~ThreadScope()
.text:000000000000D620
.text:000000000000D620 loc_D620                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+1A4↓j
.text:000000000000D620                 LDR             X8, [X20,#0x28]
.text:000000000000D624                 LDUR            X9, [X29,#var_28]
.text:000000000000D628                 CMP             X8, X9
.text:000000000000D62C                 B.NE            loc_D644
.text:000000000000D630                 LDP             X29, X30, [SP,#0x60+var_s0]
.text:000000000000D634                 LDP             X20, this, [SP,#0x60+var_10]
.text:000000000000D638                 LDP             X22, X21, [SP,#0x60+var_20]
.text:000000000000D63C                 ADD             SP, SP, #0x70 ; 'p'
.text:000000000000D640                 RET
.text:000000000000D644 ; ---------------------------------------------------------------------------
.text:000000000000D644
.text:000000000000D644 loc_D644                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+98↑j
.text:000000000000D644 this = X19                              ; const facebook::jni::JniException *
.text:000000000000D644                 BL              .__stack_chk_fail
.text:000000000000D648 ; ---------------------------------------------------------------------------
.text:000000000000D648 ;   catch(...) // owned by D5C4
.text:000000000000D648                 STR             X20, [SP,#0x60+var_60]
.text:000000000000D64C                 MOV             X20, X0
.text:000000000000D650                 ADD             X0, SP, #0x60+var_40 ; this
.text:000000000000D654                 BL              ._ZN8facebook3jni11ThreadScopeD2Ev ; facebook::jni::ThreadScope::~ThreadScope()
.text:000000000000D658                 B               loc_D664
.text:000000000000D65C ; ---------------------------------------------------------------------------
.text:000000000000D65C ;   catch(...) // owned by D5B8
.text:000000000000D65C                 STR             X20, [SP,#0x60+var_60]
.text:000000000000D660                 MOV             X20, X0
.text:000000000000D664
.text:000000000000D664 loc_D664                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+C4↑j
.text:000000000000D664                 MOV             X0, X20 ; void *
.text:000000000000D668                 BL              .__cxa_begin_catch
.text:000000000000D66C                 ADRP            X9, #_ZN8facebook3jni12JniException25kExceptionMessageFailure_E_ptr@PAGE
.text:000000000000D670                 LDR             X9, [X9,#_ZN8facebook3jni12JniException25kExceptionMessageFailure_E_ptr@PAGEOFF]
.text:000000000000D674                 ADD             X20, this, #0x10
.text:000000000000D678                 CMP             X20, X9
.text:000000000000D67C                 B.EQ            loc_D730
.text:000000000000D680                 LDRB            W10, [X9] ; facebook::jni::JniException::kExceptionMessageFailure_
.text:000000000000D684                 LDP             X11, X12, [X9,#8]
.text:000000000000D688                 LDRB            W8, [X20]
.text:000000000000D68C                 LSR             X13, X10, #1
.text:000000000000D690                 TST             W10, #1
.text:000000000000D694                 CSINC           X7, X12, X9, NE ; __p_new_stuff
.text:000000000000D698                 CSEL            X21, X13, X11, EQ
.text:000000000000D69C                 TBNZ            W8, #0, loc_D6B0
.text:000000000000D6A0                 MOV             W1, #0x16
.text:000000000000D6A4                 SUBS            X2, X21, X1
.text:000000000000D6A8                 B.HI            loc_D6C4
.text:000000000000D6AC                 B               loc_D6D0
.text:000000000000D6B0 ; ---------------------------------------------------------------------------
.text:000000000000D6B0
.text:000000000000D6B0 loc_D6B0                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+108↑j
.text:000000000000D6B0                 LDR             X9, [X20]
.text:000000000000D6B4                 AND             X9, X9, #0xFFFFFFFFFFFFFFFE
.text:000000000000D6B8                 SUB             X1, X9, #1 ; __old_cap
.text:000000000000D6BC                 SUBS            X2, X21, X1 ; __delta_cap
.text:000000000000D6C0                 B.LS            loc_D6D0
.text:000000000000D6C4
.text:000000000000D6C4 loc_D6C4                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+114↑j
.text:000000000000D6C4                 TBNZ            W8, #0, loc_D6E0
.text:000000000000D6C8                 LSR             X3, X8, #1
.text:000000000000D6CC                 B               loc_D6E4
.text:000000000000D6D0 ; ---------------------------------------------------------------------------
.text:000000000000D6D0
.text:000000000000D6D0 loc_D6D0                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+118↑j
.text:000000000000D6D0                                         ; facebook::jni::JniException::populateWhat(void)+12C↑j
.text:000000000000D6D0                 TBNZ            W8, #0, loc_D6FC
.text:000000000000D6D4                 ADD             X22, X20, #1
.text:000000000000D6D8                 CBNZ            X21, loc_D704
.text:000000000000D6DC                 B               loc_D714
.text:000000000000D6E0 ; ---------------------------------------------------------------------------
.text:000000000000D6E0
.text:000000000000D6E0 loc_D6E0                                ; CODE XREF: facebook::jni::JniException::populateWhat(void):loc_D6C4↑j
.text:000000000000D6E0                 LDR             X3, [this,#0x18] ; __old_sz
.text:000000000000D6E4
.text:000000000000D6E4 loc_D6E4                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+138↑j
.text:000000000000D6E4 ;   try {                               ; this
.text:000000000000D6E4                 MOV             X0, X20
.text:000000000000D6E8                 MOV             X4, XZR ; __n_copy
.text:000000000000D6EC                 MOV             X5, X3  ; __n_del
.text:000000000000D6F0                 MOV             X6, X21 ; __n_add
.text:000000000000D6F4                 BL              _ZNSt6__ndk112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE21__grow_by_and_replaceEmmmmmmPKc ; std::string::__grow_by_and_replace(ulong,ulong,ulong,ulong,ulong,ulong,char const*)
.text:000000000000D6F4 ;   } // starts at D6E4
.text:000000000000D6F8                 B               loc_D730
.text:000000000000D6FC ; ---------------------------------------------------------------------------
.text:000000000000D6FC
.text:000000000000D6FC loc_D6FC                                ; CODE XREF: facebook::jni::JniException::populateWhat(void):loc_D6D0↑j
.text:000000000000D6FC                 LDR             X22, [this,#0x20]
.text:000000000000D700                 CBZ             X21, loc_D714
.text:000000000000D704
.text:000000000000D704 loc_D704                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+144↑j
.text:000000000000D704                 MOV             X0, X22 ; void *
.text:000000000000D708                 MOV             X1, X7  ; void *
.text:000000000000D70C                 MOV             X2, X21 ; size_t
.text:000000000000D710                 BL              .memmove
.text:000000000000D714
.text:000000000000D714 loc_D714                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+148↑j
.text:000000000000D714                                         ; facebook::jni::JniException::populateWhat(void)+16C↑j
.text:000000000000D714                 STRB            WZR, [X22,X21]
.text:000000000000D718                 LDRB            W8, [X20]
.text:000000000000D71C                 TBNZ            W8, #0, loc_D72C
.text:000000000000D720                 LSL             W8, W21, #1
.text:000000000000D724                 STRB            W8, [X20]
.text:000000000000D728                 B               loc_D730
.text:000000000000D72C ; ---------------------------------------------------------------------------
.text:000000000000D72C
.text:000000000000D72C loc_D72C                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+188↑j
.text:000000000000D72C                 STR             X21, [this,#0x18]
.text:000000000000D730
.text:000000000000D730 loc_D730                                ; CODE XREF: facebook::jni::JniException::populateWhat(void)+E8↑j
.text:000000000000D730                                         ; facebook::jni::JniException::populateWhat(void)+164↑j ...
.text:000000000000D730 ;   try {
.text:000000000000D730                 BL              .__cxa_end_catch
.text:000000000000D734                 LDR             X20, [SP,#0x60+var_60]
.text:000000000000D738                 B               loc_D620
.text:000000000000D73C ; ---------------------------------------------------------------------------
.text:000000000000D73C ;   catch(...) // owned by D6E4
.text:000000000000D73C                 MOV             this, X0
.text:000000000000D740                 BL              .__cxa_end_catch
.text:000000000000D740 ;   } // starts at D730
.text:000000000000D744                 MOV             X0, X19
.text:000000000000D748                 BL              __clang_call_terminate
.text:000000000000D74C ; ---------------------------------------------------------------------------
.text:000000000000D74C ;   catch(...) // owned by D730
.text:000000000000D74C                 BL              __clang_call_terminate
.text:000000000000D74C ; } // starts at D594

看到類似于; __unwind { // __gxx_personality_v0 的注釋時,通常意味著該反匯編代碼塊與 C++ 的異常處理機制相關,它應該是通過讀取 .eh_frame段來獲取到對應函數的 FDE 信息從而展示出來的。

可以看到,代碼中設置了多個出口函數,分別對應不同情況下,處理各個局部變量的析構。當程序中有多個可能拋異常的地方時,landing pad也相應地會有多個,該函數的出口將更復雜,這也算是異常處理的一個開銷。

小結

總的來說,當程序拋出異常之后,會經歷以下流程:

  • 調用__cxa_allocate_exception函數,分配一個異常對象。
  • 調用__cxa_throw函數,這個函數會將異常對象做一些初始化。
  • __cxa_throw() 調用Itanium ABI里的_Unwind_RaiseException() 從而開始unwind。
  • _Unwind_RaiseException()對調用鏈上的函數進行unwind時,調用personality routine。
  • 如果該異常如能被處理(有相應的catch),則personality routine會依次對調用鏈上的函數進行清理。
  • _Unwind_RaiseException() 將控制權轉到相應的catch代碼。
  • unwind完成,用戶代碼繼續執行。

crash元兇之一

先來說一下 RN 升級到 0.72.5 版本之后觸發的SIGSEGV crash,我們上文中也提到了 ndk 降級到 21 之后,這個問題就不復存在。崩潰是發生在棧展開過程中,說明可能是 eh_frame 存儲的 FDE、CIE 數據結構發生了變化,導致棧幀信息在讀取時出現錯亂,從而導致內存訪問時出現了SIGSEGV。

接下來,就看一下 ndk 從21升至23之后,GCC版本是否發生了變化。

通過翻閱 ndk 23 版本的 Changelog,發現一條重要線索:從 ndk 23 開始,C++ 標準庫中 libunwind 從 libgcc 替換為 LLVM 中的 libunwind(也就相當于從GCC 徹底變為 clang)。

LLVM與GCC對于 libunwind 都有各自的實現,而 unwind 所使用的 eh_frame 是由編譯器所生成的(也就是上文中所介紹的 FDE 與 CIE),RN 升級后使用的 ndk 23,所以是由 llvm 的 clang 所生成的,但是的 libc++_shared.so 是 ndk 21 的,所以 fbjni 中的符號鏈接到了 GCC 中 __gxx_personality_v0 函數,最終 GCC 的 unwind 函數去處理 clang 生成 eh_frame,這勢必會出現問題,兩者之間的數據結構都不一樣。

我們看一下 LLVM 中 FDE 與 CIE 的數據結構定義。

圖片圖片

這與上文中介紹到的 GCC 版本有明顯差異,所以棧展開過程中讀取 eh_frame 出現了內存訪問錯誤,導致最終的 SIGSEGV crash,本質原因還是 RN 的動態庫與得物的 libc++_shared.so  ndk版本不一致。

crash元兇之二

上面我們介紹了ndk版本不一致導致的SIGSEGV crash,接下來,我們再來看一下crash平臺上另外一個 SIGABRT crash。

反編譯得物 libc++_shared.so 之后,通過 crash 堆棧中的偏移量可以鎖定崩潰點在這里。

圖片圖片

圖片圖片

因為打在得物 release 包中的 libc++_shared.so 是經過符號裁剪的,于是就反編譯了ndk 21 中未裁剪符號的 libc++_shared.so,通過反編譯后的代碼來鎖定崩潰點對應的源碼位置。

圖片圖片

而從源碼中來看,最終對應的則是 _Unwind_SetGR。

圖片圖片

圖片圖片

因為得物中 libc++_shared.so 是 ndk 21版本的,所以 _Unwind_SetGR 的實現是來自于 GCC 版本。

圖片圖片

而_Unwind_SetGR 的主要作用是將指定寄存器的值更新為給定的值,用來確保相關寄存器在展開期間能夠恢復到正確的狀態,而set_registers 正是通過修改寄存器,來跳轉到對應的 catch 塊。

通過上面的分析,可以將最終的錯誤鎖定到是 _Unwind_SetGR 修改寄存器出現了錯誤,而導致錯誤原因大概率就是因為dwarf_reg_size_table[index] != 8 這個判斷。

dwarf_reg_size_table是一個DWARF (Debugging With Attributed Record Formats) 相關的數據結構,用于描述寄存器的大小。在使用調試信息和異常處理時,這個信息很重要,特別是在處理棧展開和訪問寄存器狀態時。

在處理異常時,dwarf_reg_size_table可以幫助編譯器和調試工具決定如何正確地保存和恢復寄存器的狀態,以便在異常發生時順利展開堆棧。

通過反編譯后的代碼,可以發現dwarf_reg_size_table在初始化時都被填充了8,正常情況下 dwarf_reg_size_table[index] != 8這個條件理論上是不可能滿足的。所以只能是dwarf_reg_size_table中的數據出現了損壞,而dwarf_reg_size_table 為什么會出現損壞,我們之后的文章再娓娓道來。所以,第二次出現crash不是因為ndk 版本升級所引起的。

crash平臺堆棧為什么只有一行

回歸正題,為什么5.64.0 版本出現了很多MMKV的 SIGSEGV crash,而且堆棧只有一行,其實這也是 unwind 所導致的,得物的 crash 采集使用的是 xCrash,我們來看一下xCrash是如何對 native crash 進行棧回溯的。

圖片圖片

xCrash 捕獲到信號之后,會調用 xcc_unwind_get(),而 xcc_unwind_get()內部會判斷系統版本從而調用到 xcc_unwind_clang_record()。

圖片圖片

而xcc_unwind_clang_record()最終調用到了 _Unwind_Backtrace(),這里雖然函數命名里有 clang,但是最終調用的是 libc++_shared.so 中的 _Unwind_Backtrace()。沒錯,他最終調用的是GCC中_Unwind_Backtrace() 而非 clang(因為得物 libc++_shared.so 是 ndk 21 編譯出來的)。所幸,_Unwind_Backtrace() 下方有個 0 == self.buf_used的判斷,如果 self.buf_used 等于 0,說明 _Unwind_Backtrace()失敗了,就會使用 sig_pc 來記錄觸發 signal 所在 pc 的棧幀信息(也就是最后一個棧幀),所以crash平臺上只有一行堆棧信息。

我們來看一下 GCC  _Unwind_Backtrace()的具體實現。

圖片圖片

有沒有看到熟悉的身影uw_frame_state_for,上文中介紹過它的作用主要是從 eh_frame 中讀取 FDE 與 CIE,因為MMKV是通過 ndk 25 編譯的,所以它的 eh_frame 是通過 LLVM Clang 所生成的,而 LLVM 中的 FDE、CIE 與 GCC 中定義是有所差異的,所以讀取解析時大概率會出錯,從而導致_Unwind_Backtrace()失敗(這里其實是有風險的,因為數據結構不一樣,uw_frame_state_for在讀取時可能會出現 SIGSEGV, 從而導致二次崩潰),最終記錄的堆棧只有一行。

其實,堆棧只有一行的不止是MMKV,crash 平臺上 hawk 相關的一部分 crash 也是一行堆棧,這是因為 hawk 是通過 ndk 27 編譯的,所以大概率也是這個原因導致的。

Android系統unwind

xCrash是指望不上了,希望只能寄托給 Android 系統的 unwind了,而系統的 unwind 所使用的是 libunwindstack,雖然它也是借助 eh_frame 來進行棧展開的,但是它對于 eh_frame 的解析是存在兼容性的,所以它能夠回溯出完整的堆棧。但是它回溯出的堆棧最終是記錄到了 tombstone 文件,所以查看完整堆棧完全寄托于上報的 tombstone 文件。不負所望,tombstone文件確實記錄了完整的堆棧。

圖片圖片

可以看到,原因是 hermes_executor.so 中拋出了一個C++ 異常,但是在棧展開過程中使用__gxx_personality_v0出現了 crash,這和上面所介紹到的第一個 crash 是相似的問題,但問題在于使用 __gxx_personality_v0為什么來自MMKV。通過堆棧,我們也找到了拋出異常的相關代碼。

圖片圖片

這個異常是 RN 加載拆包文件過程中,拆包文件出現了損壞導致了配置解析出錯拋出的異常,在之前版本中,這個問題都是作為JS異常上報到了 sls,但是從 5.64.0 開始,這個C++ 異常捕獲不住了,直接變為了 crash,根本原因還是出在了__gxx_personality_v0。從堆棧中可以看出,__cxa_throw使用的還是 c++_shared.so 中的,但是棧展開過程中使用__gxx_personality_v0卻是mmkv.so中的,上文中也介紹過 fs.personality 來自于 CIE,說明棧回溯過程中某一個棧幀的 fs.personality 指向了 mmkv.so 中的 __gxx_personality_v0,也就是某個動態庫中引用的 __gxx_personality_v0符號鏈接到了mmkv.so(因為MMKV是已靜態庫的方式集成了libc++,所以也導出 libc++ 的符號)。

因為拆包加載要經過了一次線程切換,切換至 RN 的JS線程,所以要使用到 fbjni 的 JNativeRunnable post 到JS線程的 MessageQueue 中,所以整個堆棧中就只有 fbjni 和 hermes_executor 的棧幀。但是 fbjni、hermes_executor 與MMKV沒有什么依賴關系,為什么會鏈接到MMKV導出的符號,這就要了解一下Android系統中動態庫的符號鏈接過程。

三、動態庫符號鏈接

System.loadLibray 最終是通過 dlopen 來加載動態庫的,所以要想了解符號鏈接的過程,就需要去分析一下 dlopen 的具體實現。

dlopen流程

dlopen 調用了do_dlopen,而 do_dlopen 調用了 find_library,然后 find_library 調用了 find_libraries,而 find_libraries 就是動態庫加載主要流程。

圖片圖片

find_library_internal 首先通過 find_loaded_library_by_name 函數判斷目標動態庫是否已經加載,如果已經加載則直接返回對應的soinfo指針,沒有加載的話則調用 load_library 繼續加載流程,下面看 load_library 函數:

圖片圖片

圖片圖片

圖片圖片

可以看到,load_library 的結尾通過for循環將其所依賴的動態庫添加至 load_tasks,然后 find_libraries 中的 load_tasks 的 for 循環再次觸發 find_library_internal,直到所有依賴的動態庫都 load_library。

通過 load_library 完成了動態庫的加載,然后結束 find_library_internal 流程,回到 find_libraries 函數,而find_libraries 函數的最后兩步則是建立 local_group_roots 集合與鏈接符號。

圖片圖片

這里可以看到,local_group_roots 集合最終存放了 dlopen 所要加載的庫,而其依賴庫并沒有加載其中。

圖片圖片

接下來,就是最后一步鏈接符號,首先是 walk_dependencies_tree,此函數會遍歷 root 節點以及它所有的依賴節點,然后將其添加至 local_group 集合,而這里的 root 節點就是 dlopen 所要加載的動態庫。

圖片圖片

可以看到,walk_dependencies_tree 是一個廣度優先遍歷的方式。

然后就是 SymbolLookupList 對象的創建,而 SymbolLookupList 對象就是用來給 符號查找時提供的動態庫列表,其中,global_group 是全局加載的動態庫,通過 dlopen 的flag參數控制,Android中System.loadLibray默認傳值是 RTLD_NOW,所以通過 System.loadLibray 加載的動態庫都不是全局的,而 local_group 則是 dlopen 所要加載的庫以及它所有依賴的庫。

其中flag有:RTLD_LAZY RTLD_NOW RTLD_GLOBAL,其含義分別為:

RTLD_LAZY:在dlopen返回前,對于動態庫中存在的未定義的變量(如外部變量extern,也可以是函數)不執行解析,就是不解析這個變量的地址。

RTLD_NOW:與上面不同,他需要在dlopen返回前,解析出每個未定義變量的地址,如果解析不出來,在dlopen會返回NULL,錯誤為:

: undefined symbol: xxxx.......

RTLD_GLOBAL:它的含義是使得庫中的解析的定義變量在隨后的其它的鏈接庫中變得可以使用。

接下來就是遍歷 local_group 中的動態庫,進行逐一鏈接,如果對應的動態庫已經鏈接過了,就不會再通過 link_image 進行鏈接。

link_image 的實現細節就不展開介紹了,就直接看一下鏈接過程中的符號查找。

圖片圖片

可以看到,符號查找是遍歷 lookup_list 中的動態庫來進行逐一查找,如果對應符號找到了,則直接 return。

而這里的lookup_list就是上面所提到的SymbolLookupList對象,里面包含了dlopen 所要加載的庫以及它所有依賴的庫。

而 hawk 依賴了MMKV,也依賴了 yoga,而 yoga 依賴了 fbjni,而 fbjni 依賴了 libc++_shared.so。

剛剛上面也提到了 SymbolLookupList 是以廣度優先遍歷的方式創建而來的。

所以 SymbolLookupList 對象中的依賴庫順序則是 hawk、MMKV、yoga、fbjni、libc++_shared.so。

上文中也可以看到,local_group 中的動態庫符號鏈接用的是同一個 SymbolLookupList 對象,所以對于 fbjni 的符號鏈接,也是如此,因為MMKV與 libc++_shared.so 都有導出C++ 符號, 所以 fbjni 最終優先鏈接到了MMKV中的C++ 符號。

為什么只有線上包才能復現

crash 出現后,線下包總是復現不了(包括打包機上 tf 包),只有最終的線上的 PR 包才能復現。于是就對 System loadLibray 的日志進行了分析,發現線下包的 fbjni 加載時機是早于 hawk 的。通過排查,發現是因為 du_developer 組件的緣故,因為啟動流程中,du_developer 中的 flipper 初始化要早于 hawk 的初始化,而 flipper 的動態庫依賴 fbjni,致使 fbjni 提前加載。后續 hawk 初始化時,fbjni 已經加載,就不會再進行符號鏈接,所以 fbjni 符號鏈接是正常的。這也導致了線下包測試過程中沒有出現過一次問題,直到線上包打出后才有小伙伴反饋。

四、問題修復

方案一:裁剪掉MMKV中的C++ 符號,MMKV本身就不應該導出C++符號,C++依賴應該使用  libc++_shared.so,如果一個庫同時依賴了MMKV和 其他依賴C++ 共享庫的 lib(如 fbjni),就會導致這個依賴C++ 共享庫的 lib 鏈接到MMKV。

方案二:確保和MMKV同時依賴或間接依賴的動態庫(自身依賴C++ 共享庫)能夠提前加載,不與被依賴庫同時進行鏈接。

顯然,方案二是改動最小的也是風險最小的,但它始終不是一個長久之計,后續如果又有其他庫出現此問題,總不能把本該懶加載的動態庫都放到應用啟動流程進行提前加載。而且,它還有另外一個隱患,導致編譯時錯誤的符號引用。就比如 hawk,hawk 也是靜態庫的方式集成了 libc++,對于 hawk 來說,C++ 相關的符號都應該是內部符號。但是實際編譯后C++ 相關的符號卻變為外部符號,這是因為編譯時,MMKV作為 hawk 的依賴,編譯工具以為 hawk 中使用的C++ 函數是來自于MMKV 而非 libc++,這也就導致 hawk 運行時調用 libc++ 的函數其實都是MMKV中的。因為 hawk 與MMKV的 ndk 版本不一致,也算是隱患之一。

但是為了能夠先解決線上問題,在 5.65.0 版本上先上線了方案二。5.65.0 版本上線后,問題確實不復存在了,但是出于后續穩定性的考量,方案一還是要做的。

MMKV符號裁剪雖然很簡單,但是問題在于它的影響范圍比較大,因為MMKV是得物 App 進程啟動加載的第一個動態庫,如果出了問題,熱修和啟動回滾都搞不定,所以要想一個萬全之策,確保線上出現問題后能夠及時回滾。

MMKV動態下發加載

為了保證MMKV能夠回滾,裁剪后的MMKV動態庫不直接替換,而是通過 yeezy 平臺進行動態下發。MMKV初始化有多個 重載方法,其中部分重載函數支持傳入自定義的 LibLoader,通過 LibLoader 自實現MMKV動態庫的加載。LibLoader 剛好滿足我們的訴求,啟動時先自行加載裁剪符號后的MMKV動態庫,如果加載失敗,則使用apk 內原本的MMKV動態庫。

圖片圖片

啟動白屏

功能提測后,有測試反饋啟動時白屏卡死,而且還只在那一臺手機上出現。起初還以為是系統問題,通過排查發現,測試的那臺手機會 mmkv.so 加載時機會更早,早于 OptimizedApplication 中的 initMMKV()。

通過調試發現,du_developer 組件中也有MMKV的初始化邏輯,而且它的初始化時機更早,之所以只在這一臺手機出現,因為這段代碼邏輯有一個判斷,只有命中灰度的設備才會執行。因為我只修改了OptimizedApplication 中的 initMMKV(),所以其他地方的MMKV初始化是不會傳入自定義的 LibLoader,這就導致了 du_developer 組件中邏輯會優先初始化MMKV,所以它先加載了 apk 中內置的 mmkv.so,然后走進OptimizedApplication中的 initMMKV(),加載了動態下發的 mmkv.so。為什么兩次加載 會導致白屏呢?這是因為兩次加載的 so 來自于不同版本的 MMKV,apk 中使用的MMKV版本是 1.3.4,而動態下發的MMKV是 1.3.13版本,那為什么兩個不同版本的MMKV加載會導致白屏呢?

du_developer 組件中的 MMKV 初始化完成之后,會初始化一個MMKV對象,而這個對象其實就是C++ 側 MMKV 對象的句柄。后續OptimizedApplication中的 initMMKV() 會再次加載 mmkv.so,在 JNI_OnLoad 中會把 Java 側的 jni 方法綁定到新加載的MMKV上。隨后,其他地方又使用之前 du_developer 組件初始化的 MMKV對象進行 getString(),而此時 getString() 對應 jni 方法已經綁定到新加載的 mmkv.so 上了。

圖片圖片

MMKV的getString() 函數 會使用線程鎖 m_lock 來保證線程安全,而這個 m_lock 則是MMKV對象里的屬性,所以最終是新方法用到了老對象里面的 m_lock。因為兩個 mmkv.so 版本不一致,升級后的MMKV中 MMKV 類結構發生了變化,因為C++ 對象最終是通過偏移量獲取屬性的,而這個對象結構發生了變化,通過新的類結構偏移量 從 老的對象里面取屬性,取出來的屬性肯定是錯誤的,所以此時的 m_lock 其實并不是一個鎖對象,從而導致線程鎖卡死在了這里。

所以,引發此問題的關鍵因素就是MMKV兩次初始化動態庫加載不一致,為了解決這個問題,需要對MMKV初始化所有調用點進行統一,所以就通過 ASM 插樁的方式對所有MMKV初始化調用點進行了修改,確保MMKV每次初始化都使用相同的LibLoader。

圖片圖片

Android 5.1閃退

功能灰度前,又有一個測試反饋 Android 5.1 上會出現MMKV的 crash,真是屋漏偏逢連夜雨。

圖片圖片

因為 hawk 是在C++ 側依賴了MMKV,所以在代碼中直接調用 getMMKVWithID()來獲取MMKV實例。

圖片圖片

而最終問題則是出現在這里,g_instanceDic 是一個空指針。

圖片圖片

g_instanceDic 是在 MMKV 的initialize() 中new出來的,而 hawk 代碼在執行時,MMKV 肯定已經初始化過了,理論上 g_instanceDic 應該不可能為空指針。難道是 hawk.so 加載時,又觸發了 apk 內置的 mmkv.so 的加載,導致 hawk.so 鏈接的符號是新加載的 mmkv.so 中的符號,因為 hawk 在使用MMKV中沒有調用初始化(因為正常情況下,應用啟動時MMKV 已經初始化過了),所以這里新加載的 mmkv.so 是沒有經過初始化的,所以此時的 g_instanceDic 是一個空指針。

那為什么 Android 5.1 上的 hawk.so 會觸發apk內置的 mmkv.so再次加載呢?這其實是 Android 5.x 系統的一個缺陷,上文 dlopen 流程中其實介紹過 find_loaded_library_by_name(),當動態庫加載時,會先通過find_loaded_library_by_name() 檢查動態庫是否已經加載過,如果已經加載過,就不會再進行加載。但是問題就出在這里,我們先來看一下Android 5.1中 find_loaded_library_by_name() 實現。

圖片圖片

當 hawk 加載觸發MMKV加載時,此處 search_name 其實是 libmmkv.so,而 si->name 是什么呢?

圖片圖片

圖片圖片

在Android 5.1 上,si->name 其實就是 load_library 傳入的名字,而我們在自定義 MMKV LibLoader 時,System.load() 傳入的so文件的全路徑,si->name就是 動態下發的so文件的全路徑。所以 find_loaded_library_by_name() 時沒有匹配成功,導致 hawk 加載時又觸發了 apk 內置的mmkv.so 的加載,從而導致了 g_instanceDic 空指針。

接下來,我們再來看一下Android 5.x 之后的版本 find_loaded_library_by_name() 是如何實現的。

圖片圖片

可以看到,Android 5.x 之后的版本在比較時不再使用 si->name,而是使用 si->get_soname(),我們再來看一下 si->get_soname()又是從何而來。

圖片

所以,最終的 soname_ 是來自于 soinfo::prelink_image() 時,從 so 文件解析出來的,我們可以看一下 mmkv.so 文件中 DT_SONAME 的對應值。

圖片

DT_SONAME 的對應值正是libmmkv.so,所以在 Android 5.x 以上的系統,find_loaded_library_by_name() 執行時 soname 會匹配成功,不會導致 apk 內置的 mmkv.so 被加載,所以不會出現上文中的空指針問題。

最終,通過多個版本的灰度,符號裁剪后的mmkv.so 也是在 5.74.0 版本內置到 apk 中全量上線了。

五、結語

C++ 異常捕獲機制能讓我們像Java 一樣通過異常來處理程序中的錯誤,提高代碼的可讀性和可維護性,雖然好用,但是也要謹慎使用。異常頻繁拋出可能會帶來性能問題,上文中也介紹了棧展開的流程,它會帶來一定的性能開銷。其次,就是包體大小的影響,每處 try catch 都會生成多個 landing pad,所以對包體大小也有一定增加。如果項目中多個動態庫編譯所使用 ndk 版本不一致,更要謹慎使用 try catch,尤其是跨 so 之間的異常捕獲,避免出現上文中所出現的問題。如果條件允許,盡量統一應用內所有動態庫編譯時所用的 ndk 版本。

責任編輯:武曉燕 來源: 得物技術
相關推薦

2012-09-11 11:38:20

小游戲云計算

2015-10-21 16:25:40

2019-04-26 14:31:27

物聯網電子商務IOT

2015-08-13 11:08:02

京東創新測試

2017-11-20 09:08:13

HPC機器學習DNN

2020-04-09 16:16:33

新基建智慧城市物聯網

2018-01-23 13:57:46

AI

2018-06-13 13:25:01

2021-04-06 06:02:51

denoVite 工具

2010-02-02 11:16:28

C++異常

2021-09-26 09:40:25

React代碼前端

2023-03-11 00:16:08

2021-11-26 00:05:19

人工智能無線傳感

2021-11-18 14:40:30

人工智能無線傳感

2017-12-15 12:49:50

2013-05-14 11:13:40

動態捕獲PythonPython異常

2017-03-21 16:34:38

iOS捕獲異常

2010-02-01 15:01:34

C++拋出異常

2010-01-27 15:36:54

C++異常處理

2021-03-13 17:38:51

Python警告開發
點贊
收藏

51CTO技術棧公眾號

欧美麻豆久久久久久中文| 欧美日韩精品一区二区三区| 99高清视频有精品视频| av资源免费观看| 成人在线丰满少妇av| 欧美日韩一区二区三区四区| 日韩一级片一区二区| 免费在线性爱视频| 九九精品视频在线看| 992tv在线成人免费观看| 国产一级淫片久久久片a级| av不卡一区| 欧美日韩一区二区电影| 日本福利视频一区| 米奇777四色精品人人爽| 成人精品国产一区二区4080| 国产精品视频资源| 日韩欧美一级视频| 午夜日本精品| 中文字幕在线国产精品| 亚洲av网址在线| 精品午夜视频| 欧美在线制服丝袜| www.com毛片| 欧美14一18处毛片| 中文字幕一区二区三| 欧美一级二级三级九九九| 国产成人久久精品77777综合 | www.精品久久| 日本欧美在线看| 欧美亚洲第一页| 国产一级做a爰片在线看免费| 91久久夜色精品国产按摩| 日韩毛片中文字幕| 星空大象在线观看免费播放| 欧美日韩黄网站| 欧美久久久影院| 最近中文字幕一区二区| 在线天堂中文资源最新版| 亚洲一区二区在线免费看| 久久99国产精品一区| 成a人v在线播放| 国产片一区二区| 免费看国产精品一二区视频| 国产91免费看| 99视频精品在线| 国产精品一区二区三区不卡| 性欧美一区二区三区| 久久99精品国产麻豆不卡| 国产精品久久久久免费a∨大胸| 国产乱国产乱老熟| 久久xxxx| 日本免费一区二区三区视频观看| 日韩欧美视频在线免费观看| 亚洲福利久久| 欧美洲成人男女午夜视频| 久久国产黄色片| 久久国产精品毛片| 国产suv精品一区二区| 手机看片久久久| 先锋影音久久久| 奇米4444一区二区三区| 亚洲欧美日韩激情| 日本色综合中文字幕| 国产一区在线播放| aaa级黄色片| 成人手机电影网| 精品国产一二| 国产在线自天天| 国产精品久久久久婷婷二区次| 正在播放精油久久| 中国av在线播放| 亚洲18女电影在线观看| 亚洲色成人一区二区三区小说| 久久精品女人天堂av免费观看| 日本精品一级二级| 8x8x成人免费视频| 中文字幕一区二区三区四区久久| 亚洲国产精品字幕| 一级特黄曰皮片视频| 亚洲精品国产首次亮相| 性色av一区二区咪爱| 99re这里只有精品在线| 日韩不卡一区二区| 亚洲va码欧洲m码| 日韩在线视频免费| 国产欧美精品国产国产专区| 中文字幕在线乱| 岛国av在线播放| 欧洲一区二区av| 黑人无套内谢中国美女| 伊人久久大香线蕉无限次| 日韩中文字幕视频在线| 久久久久久久久久久网 | 国模精品一区| 欧美成年人视频网站| 欧美三级韩国三级日本三斤在线观看| 首页国产欧美久久| 97久久夜色精品国产九色| 免费在线视频你懂得| 亚洲精品视频在线| 国内外免费激情视频| 日韩免费一级| 国产亚洲精品美女久久久| 免费视频网站www| 日本亚洲免费观看| 国产日韩欧美精品| 欧美性天天影视| 狠狠久久五月精品中文字幕| 加勒比av中文字幕| 伊人成综合网yiren22| 欧美麻豆久久久久久中文| 波多野结衣高清在线| 成人爱爱电影网址| 成年丰满熟妇午夜免费视频| 播放一区二区| 国产视频精品xxxx| 久久久全国免费视频| 麻豆久久久久久久| 欧美一区免费视频| 9999热视频在线观看| 91精品一区二区三区在线观看| 日本一区二区三区网站| 精品动漫3d一区二区三区免费| 成人黄色免费看| 成人高清免费观看mv| 日韩欧美成人精品| 水蜜桃av无码| 亚洲日韩视频| 超碰在线观看97| 26uuu亚洲电影在线观看| 欧美日韩电影一区| 在线观看免费黄色网址| 久久综合九色| 青娱乐一区二区| 欧产日产国产精品视频 | 91超碰rencao97精品| 日本激情视频在线观看| 欧美日韩亚洲综合在线 欧美亚洲特黄一级| 久久性爱视频网站| 韩日在线一区| 国产精品三区四区| 草美女在线观看| 亚洲成年人在线播放| 黄色一级视频免费观看| 国产高清精品在线| 国产在线视频在线| 一区二区视频| 午夜欧美大片免费观看| 偷拍精品一区二区三区| 亚洲电影第三页| 日韩精品视频一区二区| 亚洲激情综合| 久久精品五月婷婷| 欧美一区国产| 在线精品播放av| 在线播放精品视频| 成人免费在线观看入口| 中文字幕第10页| 国产精品v日韩精品v欧美精品网站| 1卡2卡3卡精品视频| 暧暧视频在线免费观看| 日韩av在线影院| 无码人妻一区二区三区线| 欧美国产一区在线| 亚洲第一天堂久久| 国产在线不卡| 久久涩涩网站| 久久亚洲资源中文字| 大量国产精品视频| 黄色片一区二区三区| 色综合久久久久综合体| 国产一区二区三区四区在线| 久久国产福利国产秒拍| 91精品国产吴梦梦| 牛牛影视久久网| 国产精品国产三级国产专播精品人 | 毛片基地黄久久久久久天堂| dy888午夜| 欧美18xxxx| 国产精品伦子伦免费视频| av免费在线免费观看| 亚洲第一页自拍| 无码无套少妇毛多18pxxxx| 国产精品黄色在线观看| 免费国偷自产拍精品视频| 国产精品主播| 樱花www成人免费视频| 成人自拍在线| 国产精品久久久久久久久男| av大片在线| 国产视频久久久久| 国产精品国产精品国产专区| 亚洲国产精品久久久久婷婷884 | 91大片在线观看| 亚洲校园激情春色| 久久不射电影网| 欧美孕妇性xxxⅹ精品hd| 日韩一级精品视频在线观看| 久久国产视频精品| 亚洲欧美日韩一区| 国产真人做爰视频免费| 国产精一品亚洲二区在线视频| 岳毛多又紧做起爽| 国内精品久久久久久久97牛牛| 日本一区二区在线| 成人台湾亚洲精品一区二区 | 免费看一级大黄情大片| 婷婷亚洲图片| 欧美一区2区三区4区公司二百 | 三区精品视频| 亚洲精品18| 成人免费在线视频网站| 成人性生活av| 韩国v欧美v日本v亚洲| 粗大黑人巨茎大战欧美成人| 亚洲欧洲一区二区三区在线观看| 亚洲精品人妻无码| 91精品国产综合久久精品| 国产乱码77777777| 欧美日韩一区二区三区| 日韩激情一区二区三区| 亚洲三级免费观看| 第一次破处视频| 久久噜噜亚洲综合| 国产亚洲色婷婷久久99精品91| 国产精品一区二区久久不卡| 亚欧激情乱码久久久久久久久| 性色av一区二区怡红| 激情伊人五月天| 亚洲免费高清| 精品视频在线观看一区| 国内精品久久久久久久影视蜜臀| 天堂av在线中文| 亚洲人体av| 三上悠亚免费在线观看| 99精品综合| 日韩视频在线免费播放| 久久社区一区| 一区二区三区四区欧美日韩| 精品久久久久中文字幕小说| 欧洲国产精品| 国产免费av一区二区三区| 欧美资源一区| 秋霞欧美视频| 亚洲一区二区三区精品动漫| 大胆日韩av| 国产精品99久久久久久大便| 99视频精品视频高清免费| 在线观看精品视频| 影视一区二区| 99在线精品免费视频| 亚洲欧美日韩国产一区| 91av在线免费播放| 美女尤物国产一区| 日本特黄在线观看| 成人午夜激情影院| 亚洲精品理论片| 国产日韩欧美综合在线| 91导航在线观看| 中文字幕一区三区| 精品欧美一区二区久久久久| 一区二区三区四区不卡视频| 国产极品美女高潮无套嗷嗷叫酒店| 亚洲一区欧美一区| 美女又爽又黄免费视频| 欧美无砖专区一中文字| 97超碰资源站| 精品国产不卡一区二区三区| 亚洲av片在线观看| 在线播放日韩精品| 爆操欧美美女| 97国产在线视频| 国产一区精品福利| 999热视频在线观看| 天天久久夜夜| 在线视频不卡一区二区| 伊人久久大香线蕉综合热线| 粗暴91大变态调教| 国产一区二区三区高清播放| 欧美日韩一区二区三区四区五区六区| 91丨porny丨户外露出| 欧美日韩生活片| 亚洲无线码一区二区三区| 久久久久久亚洲av无码专区| 日韩一区二区在线看| 天堂av在线免费观看| 中文字幕精品av| 黄毛片在线观看| 成人国产精品久久久久久亚洲| 国内精品免费| 亚洲一区二区三区精品在线观看| 欧美日韩综合| 天天干天天爽天天射| 成人丝袜高跟foot| 日本裸体美女视频| 一本到不卡免费一区二区| 99国产精品99| 一道本无吗dⅴd在线播放一区 | 欧美一区二区三区艳史| 亚洲伊人精品酒店| 欧美亚洲精品日韩| 欧美日韩国产在线一区| 国产区二区三区| 91丝袜美腿高跟国产极品老师| 亚洲伦理一区二区三区| 色94色欧美sute亚洲线路二| 精品人妻无码一区二区色欲产成人 | 久久激情婷婷| 一级黄色片毛片| 亚洲三级免费观看| 五月天中文字幕| 日韩激情片免费| 激情av在线播放| 亚洲a成v人在线观看| 日本一区二区高清不卡| 欧美国产激情视频| 成人激情av网| 澳门黄色一级片| 91精品国产欧美一区二区18| 国产裸舞福利在线视频合集| 91大神福利视频在线| 免费一级欧美片在线观看网站| 亚洲一区二区在线免费观看| 一区二区黄色| 国产伦精品一区二区三区88av| 亚洲视频一区在线| 91精品国产乱码久久久| 一本色道久久综合亚洲精品小说| 中文在线а√天堂| 精品一卡二卡三卡四卡日本乱码| 韩国av一区| 黑人无套内谢中国美女| 一区二区三区丝袜| 99热精品在线播放| 欧美成人全部免费| 日韩高清二区| 成人午夜免费在线视频| 国产在线播放一区| 九九视频免费看| 日韩欧美资源站| 蜜臀av在线| 官网99热精品| 日韩一区二区久久| 最近日本中文字幕| 日韩欧美国产成人| 九一在线视频| 国产精品视频一区二区高潮| 欧美日韩在线网站| 色婷婷狠狠18| 亚洲欧美另类在线| 精品久久久久久亚洲综合网站| 伦伦影院午夜日韩欧美限制| 韩国三级大全久久网站| 国产一级大片免费看| 国产成人av在线影院| 国产欧美日韩另类| 亚洲人成在线观看| 欧美激情福利| 麻豆传媒网站在线观看| 成人黄色大片在线观看 | 日本在线播放一区二区三区| 调教驯服丰满美艳麻麻在线视频| 欧美日韩国产一级片| gogo在线高清视频| 国产精品久久亚洲| 爽好久久久欧美精品| 国产人与禽zoz0性伦| 欧美一区二区三区成人| aaa在线播放视频| 午夜精品一区二区三区在线观看 | 人妻熟女aⅴ一区二区三区汇编| 欧美性高潮在线| 日本天堂在线观看| 国产欧美韩日| 日本欧美一区二区| 青青操国产视频| 亚洲欧美中文日韩在线| av在线精品| 97国产精东麻豆人妻电影| 国产欧美日产一区| 精品人妻无码一区二区三区蜜桃一| 97成人精品视频在线观看| 日韩大片在线播放| 少妇搡bbbb搡bbb搡打电话| 一本大道av一区二区在线播放| 黄色成人在线观看| 久久综合九色欧美狠狠| 国内成人精品2018免费看| 日韩在线视频免费播放| 日韩在线视频国产| 欧美成人一区在线观看| 色噜噜狠狠一区二区| 五月综合激情网| 视频三区在线| 久久伊人一区| 粉嫩一区二区三区在线看| 中文字幕理论片| 97成人在线视频| 欧美在线亚洲|