一次精疲力盡的改bug經曆,精疲力盡改bug經曆

來源:互聯網
上載者:User

一次精疲力盡的改bug經曆,精疲力盡改bug經曆
一、介紹

最近一直在做有關JavaScriptCore的技術需求,上周發現一個問題,當在JavaScriptCore在記憶體回收時,項目會有一定幾率發生崩潰。崩潰發生時呼叫堆疊如下:


圖1 呼叫堆疊

先對中兩個比較重要的堆棧過程做個說明:


圖2 產生JSValue

1)、toJSValueInContext:方法是通過JSObjectMake 再產生一個JSValue。如中,最終返回的是一個JSValue,並且這個JSValue對self(PHOValue類型)做了一次強引用。


圖3 該JSValue釋放回調

2)、PHOObject_finalizeCallback 是JSValue的解構函式,當通過JSObjectMake產生的JS對象在釋放時會調用該函數。在這個函數中,我們釋放了之前所強引用的self(PHOValue類型)。當self釋放時,self所強持有的對象A會被釋放。進一步執行A的dealloc方法中,在dealloc方法中,我們再次調用了JSObjectMake函數產生其他的對象,並再次強持有了A對象,並將JSValue傳入到JS中進行其他方法調用(如果不理解這個問題,請參考JSPatch對重寫dealloc方法的處理,但是不同的是JSPatch 並不依賴記憶體回收)。

為了說明問題,特地畫了個記憶體流程簡圖輔助理解:


圖4 記憶體情況和流程說明二、定位問題

為了定位問題,我們進行了很多猜想,在這裡我們列舉兩個比較有代表性的猜想。

猜想1:在dealloc中不允許對正在執行dealloc的對象進行強引用

由於這個問題是有一定的機率出現,並且報出了Thread 1: EXC_BREAKPOINT (code=EXC_I386_BPT, subcode=0x0)這樣的錯誤,因此我們最開始一直將精力集中在追查野指標上。崩潰發生在self進行dealloc的時機,但是在這個時機我們對self又做了一次強引用(見圖2代碼)。此時會對self的引用計數+1,因此猜測可能會重複觸發self的dealloc。但是實際上當崩潰發生時,po操作查看self,context 等參數,發現所有的參數都是正常允許訪問的。並且這與呼叫堆疊的現象並不相符,至少我們沒有看到兩次調用dealloc。因此這種猜想是不成立的。

猜想2:JavaScriptCore 在進行記憶體回收時不允許進行JSObjectMake

從呼叫堆疊來看,每次崩潰都發生在JSObjectMake之後,這是不是意味著記憶體回收時不能進行JSObjectMake操作呢?為了驗證這個問題,我們在PHOObject_finalizeCallback函數中不做任何對象釋放操作,僅僅執行一次JSObjectMake,


圖5 回調中調用JSObjectMake

這樣的改動就意味著,只要處於JavaScriptCore進行記憶體回收,就會立刻調用JSObjectMake。經過驗證發現,果然在此處發生崩潰,並且是百分百複現,呼叫堆疊基本一致。因此可以說明我們的猜想是正確的。仔細想想這個問題,有經驗的同學可能會感到細思極恐,因為記憶體回收機制並不受我們控制,我們在進行JSObjectMake無法保證一定不處於記憶體回收期間,那麼理論上來說應該進行發生崩潰才對,為什麼這個問題之前一直沒有暴露出來呢?我們迴圈100000次建立對象並不斷通過safari的調試功能人工觸發記憶體回收,並沒有發生崩潰。JavascriptCore存在兩種記憶體回收方式,一種是同步回收,一種是非同步回收,無論哪種方式,JavascriptCore對虛擬機器有共有的堆(Heap,JavascriptCore的記憶體回收處理都在Heap.cpp中)都進行了加鎖處理,換句話說就是在正常情況下JSObjectMake在記憶體回收時是無法訪問堆的。


圖6 JSCore的兩種記憶體回收方式

而我們之所以發生崩潰是由於我們在對象在記憶體回收的回調中訪問了堆,這個問題的虛擬碼如下:

 


圖7 虛擬碼三、尋找解決方案

既然基本定位到了問題的原因,那麼下一步就要找方法去解決這個問題。問題的根源在於我們想在JS變數釋放的時候釋放它所間接持有的OC對象,如果在記憶體回收期間我們無法進行釋放,那麼是不是意味著只要我們擷取到JavascriptCore的記憶體回收開始和結束回調就能避免這個問題了呢?尋找JavascriptCore後發現,還真的有這個回調狀態,只不過介面並沒有對我們開放,Heap.h中存在一個添加觀察者的介面。


圖8 添加觀察者

當即將進行記憶體回收和記憶體回收結束後會通知觀察者:


圖9 開始回調

 


圖10 結束回調

那麼現在問題來了,我們既然知道了回調方法,那麼如何獲得回調呢?在OC層面,我們可以通過runtime 進行hook,甚至在C語言層面我們也可以通過fb的fishhook來實現hook,在C++層面我們如何hook一個帶命名空間的函數呢?(這個問題我們並沒有實現思路,如果有人知道在iOS中如何hook一個C++函數,請及時留言指教)。在經曆了一系列嘗試後,我們放棄了hook C++函數的方法,轉而尋求其他方法。回到最初的目的,實際上我們就是想保證記憶體回收之後再執行我們的JSObjectMake。因此GCD的延遲操作是一個很好的思路,但是到底延遲多長時間呢?這個方案似乎不是那麼完美。那麼還有什麼操作是一個延遲釋放的操作呢?__autoreleasing 應該是一個比較好的選擇。當對象前被添加__autoreleasing修飾時,這個對象會被延遲到自動釋放池釋放時才被釋放。當自動釋放池釋放時當前runloop一定是結束了,也就是說該記憶體回收一定是結束了(不可能一次記憶體回收分為兩個runloop)。因此只需要將代碼改為如下所圖11示即可


圖11 修改方案四、總結

這個問題還是比較難定位的,首先是很難定位到記憶體回收導致問題,其次是很難找到比較好的回調,尤其是hook c++函數,我們做了很多次嘗試都沒有成功。如果有人有過在iOS系統中hook C++函數的實現方案,請不吝賜教,多謝多謝!

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.