iOS App自動監(jiān)控Zombie對象方案

iOS開發(fā)過程或者線上版本經(jīng)常有Crash崩潰在objc_msgSend、objc_retain、objc_release等方法,這些都是典型等Zombie問題,在開發(fā)過程可以使用Instruments工具定位,但對于線上問題或者很難復(fù)的問題Instruments就很難定位了,如果能主動捕捉Zombie對象,并且Trace Zombie對象信息和釋放棧,那就很容易分析問題了。本文介紹一種主動監(jiān)控Zombie對象方案,方案已經(jīng)上線驗證一段時間了,并且已經(jīng)在github上開源了。

為什么使用ARC還會有Zombie問題?

可能很多同學(xué)覺得使用ARC和weak屬性后就不會有Zombie問題了,但App上線后還是會發(fā)現(xiàn)很多Zombie問題,主要是因為:

  • 還有很多地方使用assign
    由于歷史原因,系統(tǒng)庫還有地方使用assign,最典型的就是iOS8下UITableView delegate和dataSource,相信大部分iOS開發(fā)都遇到過這個問題導(dǎo)致的Crash,還有就是自己都代碼也可能使用assign。
  • 線程安全問題
    雖然ARC下可以不用考慮對象釋放問題,但如果不是線程安全的話,就算使用weak還是可能導(dǎo)致Zombie問題。

如何分析Zombie問題?

大多數(shù)Zombie問題都是Crash在objc_msgSend方法,一般都是消息轉(zhuǎn)發(fā)的時候發(fā)生EXC_BAD_ACCESS錯誤,但從堆棧上無法看出是什么對象、什么方法出了問題,并且很多時候也不是Crash在我們自己但代碼附近,這種情況就更難分析了。
我們知道調(diào)用objc_msgSend方法的時候前面幾個參數(shù)是通過x0~x7寄存器傳遞的,其中x0是self指針,x1是SEL,如果能知道Crash在哪個SEL,結(jié)合代碼有可能進(jìn)一步分析出哪里Crash了。SEL是指向C string的指針,并且C string 保存在__TEXT段,所以結(jié)合dSYM和x1寄存器,正常情況下是可以解析出SEL的,具體解析方法可以參考「So you crashed in objc_msgSend()」。
如果SEL是很常見的方法,即使知道SEL還是很分析出問題,這個時候如果能知道Zombie對象類名,對象釋放棧的話就能很容易分析出問題了,但僅通過Crash log文件是無法獲取這些信息的,這也是大多數(shù)Zombie問題很難定位的原因。
其實除了Crash,Zombie也可能只是導(dǎo)致邏輯錯誤,這個時候就更難定位問題了,因為往往問題現(xiàn)場離對象釋放的地方很遠(yuǎn)。

自動監(jiān)控Zombie對象方案

如果能主動監(jiān)測Zombie,在第一次使用Zombie對象的時候就發(fā)現(xiàn)問題,可以進(jìn)行上報,也可以主動觸發(fā)Crash,同時在釋放對象的時候保存對象信息,可以知道到底是哪個對象出現(xiàn)了問題,這樣可以極大的提高Zombie問題的發(fā)現(xiàn)率和解決率。
要在線上版本實時監(jiān)控,監(jiān)控組件必須對App性能影響很小,在設(shè)計對時候從下面點考慮:

  • Trace Zombie對象類名、selector、釋放棧信息
  • 內(nèi)存可控
  • 監(jiān)控策略可控
  • 對cpu影響要很小

如何監(jiān)控

要監(jiān)控Zombie對象,就必須監(jiān)控訪問已經(jīng)釋放對對象,所以可以從下面幾個點思考:

  1. 如何監(jiān)控對象釋放
    Objective-C上可以方便的使用Method swizzling hook dealloc方法監(jiān)控對象的釋放,Method swizzling只能監(jiān)控Objective-C對象的釋放;也可以在更底層hook free,使用Fishhook可以很方便的hook free方法,hook free可以監(jiān)控所以對象的釋放。
  2. 如何監(jiān)控訪問已經(jīng)釋放的對象
    可以使用Objective-C Runtime消息轉(zhuǎn)發(fā)機(jī)制監(jiān)控Objective-C對象訪問,要監(jiān)控C/C++對象訪問復(fù)雜些,一種方法是對象釋放后使用vm_protect設(shè)置虛擬內(nèi)存為不可讀寫,不過vm_protect只能以內(nèi)存頁為單位進(jìn)行設(shè)置。

最終方案

因為iOS App上大部分自定義對象都是Objective-C對象,所以最終使用Method swizzling hook dealloc方法監(jiān)控對象的釋放,并且使用Runtime消息轉(zhuǎn)發(fā)機(jī)制監(jiān)控Objective-C對象訪問,主要過程如下:

  1. hook dealloc方法,dealloc時只析構(gòu)對象,不釋放內(nèi)存,更換isa指針指向ZombieHandler Class
  2. 延遲釋放對象
  3. ZombieHandler Class攔截消息,從而監(jiān)控使Zombie對象
    方案模塊結(jié)構(gòu)如下圖:


    Zombie監(jiān)控模塊結(jié)構(gòu)圖

內(nèi)存優(yōu)化

一開始對象釋放棧保存完整的棧,并且保存string類型,類似下面

"1 libdispatch.dylib 0x0000000021809823 0x21807000 + 10275"
"2 libdispatch.dylib 0x0000000021809823 0x21807000 + 10275"

后來改成只保存函數(shù)地址,并且arm64下每個地址只用40bit,iOS64位系統(tǒng)下每個地址其實只用了36位,使用40位方便操作,上報的時候也只上報函數(shù)地址,這樣可以極大程度的減小組件占用的內(nèi)存,優(yōu)化后棧類型下面:

dealloc stack:{
tid:1027
stack:[0x0000000100047534,0x000000010004b2e4,0x00000001000498b0,0x000000018e9bdf9c,0x000000018e9bdb78,0x000000018e9c43f8,0x000000018e9c1894,0x000000018ea332fc,]
}

其實??梢灾苯颖4嬖谘舆t釋放對象的內(nèi)存上面,這樣可以進(jìn)一步優(yōu)化內(nèi)存使用。

開源

監(jiān)控組件已經(jīng)開源,并且提供符號化腳本,使用也很簡單,只需要幾行調(diào)用就可以:

    //setup DDZombieMonitor
    void (^zombieHandle)(NSString *className, void *obj, NSString *selectorName, NSString *deallocStack, NSString *zombieStack) = ^(NSString *className, void *obj, NSString *selectorName, NSString *deallocStack, NSString *zombieStack) {
        NSString *zombeiInfo = [NSString stringWithFormat:@"ZombieInfo = \"detect zombie class:%@ obj:%p sel:%@\ndealloc stack:%@\nzombie stack:%@\"", className, obj, selectorName, deallocStack, zombieStack];
        NSLog(@"%@", zombeiInfo);
        
        NSString *binaryImages = [NSString stringWithFormat:@"BinaryImages = \"%@\"", [self binaryImages]];
        NSLog(@"%@", binaryImages);
    };
    [DDZombieMonitor sharedInstance].handle = zombieHandle;
    [[DDZombieMonitor sharedInstance] startMonitor];

組件支持下面特性:

  • 主動監(jiān)控Zombie問題,并且提供Zombie對象類名、selector、釋放棧信息
  • 支持不同監(jiān)控策略,包括App內(nèi)自定義類、白名單、黑名單、所有對象
  • 支持設(shè)置最大占用內(nèi)存
  • 組件在收到內(nèi)存告警或超過最大內(nèi)存時,通過FIFO算法釋放部分對象

性能影響和穩(wěn)定性

組件上線一兩個版本了,目前還沒發(fā)現(xiàn)Crash
cpu影響:打開Zombie檢測前后相差0.2%左右,影響很小
內(nèi)存影響:Zombie組件內(nèi)存開關(guān)為10M的時候,實際內(nèi)存增加11M左右,10M只計算了延遲釋放對象和對象釋放棧,組件本身占用內(nèi)存沒計算在內(nèi),符合預(yù)期

具體源碼請移步「github DDZombieMonitor」

參考

So you crashed in objc_msgSend()
手動分析iOS Crash log庖丁解牛

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

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

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