解決JavaScriptCore垃圾回收引起的崩潰

一、介紹

最近一直在做有關(guān)JavaScriptCore的技術(shù)需求,上周發(fā)現(xiàn)一個問題,當(dāng)在JavaScriptCore在垃圾回收時,項目會有一定幾率發(fā)生崩潰。崩潰發(fā)生時調(diào)用堆棧如下:

圖1 調(diào)用堆棧

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

圖2 生成JSValue

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

圖3 該JSValue釋放回調(diào)

2)、PHOObject_finalizeCallback 是JSValue的析構(gòu)函數(shù),當(dāng)通過JSObjectMake生成的JS對象在釋放時會調(diào)用該函數(shù)。在這個函數(shù)中,我們釋放了之前所強(qiáng)引用的self(PHOValue類型)。當(dāng)self釋放時,self所強(qiáng)持有的對象A會被釋放。進(jìn)一步執(zhí)行A的dealloc方法中,在dealloc方法中,我們再次調(diào)用了JSObjectMake函數(shù)生成其他的對象,并再次強(qiáng)持有了A對象,并將JSValue傳入到JS中進(jìn)行其他方法調(diào)用(如果不理解這個問題,請參考JSPatch對重寫dealloc方法的處理,但是不同的是JSPatch 并不依賴?yán)厥眨?/p>

為了說明問題,特地畫了個內(nèi)存流程簡圖輔助理解:

圖4 內(nèi)存情況和流程說明

二、定位問題

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

猜想1:在dealloc中不允許對正在執(zhí)行dealloc的對象進(jìn)行強(qiáng)引用

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

猜想2:JavaScriptCore 在進(jìn)行垃圾回收時不允許進(jìn)行JSObjectMake

從調(diào)用堆棧來看,每次崩潰都發(fā)生在JSObjectMake之后,這是不是意味著垃圾回收時不能進(jìn)行JSObjectMake操作呢?為了驗證這個問題,我們在PHOObject_finalizeCallback函數(shù)中不做任何對象釋放操作,僅僅執(zhí)行一次JSObjectMake,

圖5 回調(diào)中調(diào)用JSObjectMake

這樣的改動就意味著,只要處于JavaScriptCore進(jìn)行垃圾回收,就會立刻調(diào)用JSObjectMake。經(jīng)過驗證發(fā)現(xiàn),果然在此處發(fā)生崩潰,并且是百分百復(fù)現(xiàn),調(diào)用堆?;疽恢?。因此可以說明我們的猜想是正確的。仔細(xì)想想這個問題,有經(jīng)驗的同學(xué)可能會感到細(xì)思極恐,因為垃圾回收機(jī)制并不受我們控制,我們在進(jìn)行JSObjectMake無法保證一定不處于垃圾回收期間,那么理論上來說應(yīng)該進(jìn)行發(fā)生崩潰才對,為什么這個問題之前一直沒有暴露出來呢?我們循環(huán)100000次創(chuàng)建對象并不斷通過safari的調(diào)試功能人工觸發(fā)垃圾回收,并沒有發(fā)生崩潰。JavascriptCore存在兩種垃圾回收方式,一種是同步回收,一種是異步回收,無論哪種方式,JavascriptCore對虛擬機(jī)有共有的堆(Heap,JavascriptCore的垃圾回收處理都在Heap.cpp中)都進(jìn)行了加鎖處理,換句話說就是在正常情況下JSObjectMake在垃圾回收時是無法訪問堆的。

圖6 JSCore的兩種垃圾回收方式

而我們之所以發(fā)生崩潰是由于我們在對象在垃圾回收的回調(diào)中訪問了堆,這個問題的偽代碼如下:


圖7 偽代碼

三、尋找解決方案

既然基本定位到了問題的原因,那么下一步就要找方法去解決這個問題。問題的根源在于我們想在JS變量釋放的時候釋放它所間接持有的OC對象,如果在垃圾回收期間我們無法進(jìn)行釋放,那么是不是意味著只要我們獲取到JavascriptCore的垃圾回收開始和結(jié)束回調(diào)就能避免這個問題了呢?查找JavascriptCore后發(fā)現(xiàn),還真的有這個回調(diào)狀態(tài),只不過接口并沒有對我們開放,Heap.h中存在一個添加觀察者的接口。

圖8 添加觀察者

當(dāng)即將進(jìn)行垃圾回收和垃圾回收結(jié)束后會通知觀察者:

圖9 開始回調(diào)


圖10 結(jié)束回調(diào)

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

圖11 修改方案

四、總結(jié)

這個問題還是比較難定位的,首先是很難定位到垃圾回收導(dǎo)致問題,其次是很難找到比較好的回調(diào),尤其是hook c++函數(shù),我們做了很多次嘗試都沒有成功。如果有人有過在iOS系統(tǒng)中hook C++函數(shù)的實現(xiàn)方案,請不吝賜教,多謝多謝!

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

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

  • 本文由我們團(tuán)隊的 糾結(jié)倫 童鞋撰寫。 寫在前面 本篇文章是對我一次組內(nèi)分享的整理,大部分圖片都是直接從keynot...
    知識小集閱讀 15,382評論 11 172
  • 本博客主要分以下幾個方面來介紹iOS中的JavaScriptCore JavaScriptCore簡介 JavaS...
    dullgrass閱讀 4,408評論 1 38
  • 寫在前面 本篇文章是對我一次組內(nèi)分享的整理,大部分圖片都是直接從keynote上截圖下來的,本來有很多炫酷動效的,...
    等開會閱讀 14,700評論 6 69
  • 注:本文copy自http://www.itdecent.cn/p/ac534f508fb0,純屬當(dāng)筆記使用。 概...
    BookKeeping閱讀 785評論 1 3
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,030評論 0 9

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