一、介紹 最近一直在做有關JavaScriptCore的技術需求,上周發現一個問題,當在JavaScriptCore在垃圾回收時,項目會有一定幾率發生崩潰。崩潰發生時調用堆棧如下: 圖1 調用堆棧 圖1 調用堆棧 先對上圖中兩個比較重要的堆棧過程做個說明: 圖2 生成JSValue 圖2 生成JSV ...
一、介紹
最近一直在做有關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++函數的實現方案,請不吝賜教,多謝多謝!