iOS 線上野指針探測實(shí)踐與展望

為啥要做線上探測

iOS的常規(guī)崩潰數(shù)量已經(jīng)不多猾编,剩余的崩潰往往是不能穩(wěn)定復(fù)現(xiàn)或缺乏有效信息佳吞。經(jīng)過線上統(tǒng)計(jì)后我發(fā)現(xiàn)目前剩余的無法定位和解決的崩潰有60%+都是由于野指針引起舀患。各種各樣的堆棧千奇百怪忆矛,比較典型的堆棧如下:

image-20220114135115950

當(dāng)然還有其他類型的堆棧卵迂,但是這些堆棧都有一個(gè)特征:都是在release或者retain時(shí)發(fā)生了崩潰裕便。

如果你的APP是首次監(jiān)控的野指針崩潰,那么建議你首先進(jìn)行線下的模擬和復(fù)現(xiàn)见咒,Xcode提供了很多優(yōu)秀的工具:Address Sanitizer闪金、SCribbleZombie等论颅。

image-20220114140406395

但是在實(shí)踐過程中很難依靠上述工具復(fù)現(xiàn)問題哎垦,這跟APP的性質(zhì)有很大的關(guān)系。如果APP是一個(gè)聚合平臺恃疯,整合了非常多的業(yè)務(wù)漏设,并且這些業(yè)務(wù)是由各自獨(dú)立的團(tuán)隊(duì)開發(fā),有各自的入口和觸發(fā)邏輯今妄,甚至有獨(dú)立的灰度策略郑口,那我們可能連測試入口都無法找到。面對這種情況盾鳞,只能依賴線上手段去收集和排查問題犬性。

做了一些技術(shù)調(diào)研

參考的文章我列舉到了參考文獻(xiàn)中,感興趣的可以閱讀腾仅。我整理了下大體的思路可以分為2類:

  • hook free

    free時(shí)乒裆,并不釋放內(nèi)存,保留內(nèi)存推励,判斷是否為objc對象鹤耍,如果是objc對象則將對象setclass為自定義類,借助消息轉(zhuǎn)發(fā)得到堆棧和類信息验辞。

  • hook dealloc

    dealloc時(shí)判斷是否需要開啟野指針探測稿黄,如果不需要則直接釋放,否則將對象修改isa后保留并加入到內(nèi)存池中跌造,再次調(diào)用對象時(shí)會觸發(fā)消息轉(zhuǎn)發(fā)攔截到堆棧及對象類名信息杆怕。

我查看了很多業(yè)界的方案,針對OC的方案基本上大同小異壳贪,思路都是保存對象陵珍,等再次調(diào)用時(shí)觸發(fā)消息轉(zhuǎn)發(fā)攔截到堆棧及類型信息。但是如何控制內(nèi)存增長撑碴、到底對哪些類做監(jiān)控等實(shí)際問題都沒有做太多的介紹撑教。上述列舉的方案在debug階段或者灰度階段使用尚可,如果真的在線上使用醉拓,可能造成不小的性能問題伟姐。因此野指針探測如果上線收苏,核心問題是如何在保證有效覆蓋率的前提下控制性能損耗。

探索與實(shí)踐

在剛開始時(shí)愤兵,我嘗試使用通過從文章中獲取到的技術(shù)方案直接落實(shí)到項(xiàng)目中鹿霸,并且監(jiān)控了全部的OC類。調(diào)試時(shí)我發(fā)現(xiàn)APP光啟動階段就已經(jīng)耗費(fèi)了很長時(shí)間秆乳。這是由于由于監(jiān)控的類型過多懦鼠,有很多非常頻繁釋放的類型也被我們監(jiān)控和處理,導(dǎo)致性能下滑非常嚴(yán)重屹堰。因此在這里我做了多次優(yōu)化肛冶,主要手段如下:

  • 屏蔽一些無關(guān)緊要的類型

  • 在dealloc中,盡量不要做耗性能操作

  • 用線程池將大量任務(wù)分發(fā)到子線程

如何屏蔽一些無關(guān)緊要的類型扯键?

首先來回答第一個(gè)問題睦袖,如何屏蔽一些無關(guān)緊要的類型監(jiān)控。在開發(fā)階段我發(fā)現(xiàn)很多底層xpc類以及很多不曾見過的類型都被我們監(jiān)控荣刑,這顯然有些扯淡馅笙。因此為了優(yōu)化監(jiān)控的類型范圍,我做了2次改進(jìn)厉亏。首先說下第一次改進(jìn):

基于動態(tài)庫的優(yōu)化方案

眾所周知董习,我們APP的主執(zhí)行文件依賴了很多的系統(tǒng)庫,這些系統(tǒng)庫是我們所使用到的明確聲明鏈接到程序上的爱只。但是這些動態(tài)庫也會依賴其他的系統(tǒng)庫皿淋,在這里我將這些系統(tǒng)庫稱為:二級動態(tài)庫。顯然這些系統(tǒng)庫的類不是我們所關(guān)心的虱颗。

image-20220120110422679

因此需要在運(yùn)行階段排除來自二級系統(tǒng)庫的這些類沥匈。

那如何確定一個(gè)類來自哪個(gè)庫呢蔗喂?

我最先想到的是dladdr忘渔,dladdr可以在運(yùn)行階段告訴我們這個(gè)地址的詳細(xì)信息,其中就包括鏡像文件信息缰儿。那我們就能直接知道這個(gè)類是否需要監(jiān)控畦粮。但是,如果你這樣做的話就會發(fā)現(xiàn)一個(gè)非常明顯的問題乖阵,那就是:這個(gè)函數(shù)運(yùn)行簡直~太宣赔!慢!了5山!,根本無法支撐大量且頻繁的調(diào)用竞端。因此我的優(yōu)化方案是,根據(jù)先獲取類名地址贡翘,根據(jù)類名地址的區(qū)間判斷在哪個(gè)庫中。

image-20220120112457131

具體實(shí)現(xiàn)為:在啟動時(shí)讀取所有的image以及每個(gè)imageTEXT段地址范圍砰逻,然后存儲到unordermap中鸣驱。當(dāng)然這里只是列舉了大體的思路,具體實(shí)現(xiàn)時(shí)還需要處理對段遷移方案的適配蝠咆。經(jīng)過上述優(yōu)化后踊东,實(shí)際效率大幅提升。

當(dāng)然刚操,這并不是一個(gè)很完美的方案闸翅,因?yàn)樵陂_發(fā)階段我發(fā)現(xiàn)我為野指針開辟的緩存池很快就被耗盡,啟動階段菊霜,30MB的緩存池竟然6~11秒就耗盡缎脾,這個(gè)消耗速度有點(diǎn)太快,按我個(gè)人理解占卧,在常規(guī)使用下遗菠,一個(gè)對象能在緩存池中存在30秒才算及格。為此华蜒,我將每個(gè)對象的類型及每個(gè)對象的大小寫入文件辙纬,查看后發(fā)現(xiàn)盡管我們做了動態(tài)庫的過濾,但是依舊有很多我們沒有見過的類型也被納入到了監(jiān)控范圍叭喜。這個(gè)很好理解贺拣,UIKitCorelibobjc等我們常見的動態(tài)庫中依舊有很多大量的我們沒用過的類型捂蕴。因此我們需要轉(zhuǎn)換思路譬涡,采用更精細(xì)的監(jiān)控:只監(jiān)控我們用到的系統(tǒng)類。

基于Bind信息的優(yōu)化方案

監(jiān)控我們用到的系統(tǒng)類的難點(diǎn)在:如何獲取到項(xiàng)目中用到的所有系統(tǒng)類啥辨?這一步可以參考我的另一篇文章:從野指針探測到對iOS 15 bind 的探索 (文章比較長涡匀,耐著性子讀一下應(yīng)該會有所收獲),在這里不再重復(fù)溉知。

dealloc中千萬不要做的事情

方案的整體思路是hook dealloc陨瘩,因此我們不可避免地要在dealloc階段注入我們的代碼,這里有幾個(gè)小的注意點(diǎn):

  • 不要調(diào)用任何OC代碼

  • 不要使用objc_setAssociatedObject

  • 不要直接上來一頓操作级乍,先判斷下isTaggedPointer

第一點(diǎn)很容易理解舌劳,因?yàn)槟阏{(diào)用了任何OC代碼都可能導(dǎo)致在dealloc中繼續(xù)引起額外的對象釋放,而這些對象釋放有可能又被你納入到監(jiān)控范圍玫荣。

第二點(diǎn)可能很多同學(xué)想象不到甚淡,不使用objc_setAssociatedObject是因?yàn)?code>objc_setAssociatedObject有不小的內(nèi)存消耗(約96B)有在大量暴力使用時(shí)才能發(fā)現(xiàn)。

第三點(diǎn)是可能很多方案沒有提到的捅厂,TaggedPointer我們沒有必要做更進(jìn)一步的監(jiān)控浪費(fèi)緩存池贯卦。關(guān)于TaggedPointer的介紹可以參考字節(jié)APM的文章:Tagged Pointer對象安全氣墊為何會失效

多線程與內(nèi)存優(yōu)化

多線程處理

開發(fā)階段底挫,由于各項(xiàng)性能指標(biāo)都不理想,因此將批量釋放對象以及對象入池包裝為Task加入到任務(wù)隊(duì)列中脸侥,多線程處理Task建邓。我已經(jīng)忘記了當(dāng)時(shí)這么處理是因?yàn)槟膲K性能問題了??,印象中好像是不這么處理掉幀明顯睁枕,最近好奇把對象入池同步處理也沒發(fā)現(xiàn)有明顯掉幀現(xiàn)象官边,比較尷尬??。

image-20220120130415835

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

內(nèi)存上的優(yōu)化主要在捕捉堆棧上外遇。野指針探測實(shí)踐就會發(fā)現(xiàn)注簿,單單知道類名以及野指針發(fā)生時(shí)的堆棧是不夠的。作為開發(fā)者跳仿,我還想知道這個(gè)對象到底是在什么時(shí)候釋放的诡渴。因此野指針探測我加了記錄釋放堆棧的功能,當(dāng)然這個(gè)功能不會全量開放菲语,僅針對配置的指定類型進(jìn)行記錄妄辩。這里有2個(gè)比較有意思的問題:

  • 堆棧的大小是否能優(yōu)化?

  • 抓取堆棧時(shí)能否用memcpy替代vm_read_overwrite山上?

所謂的堆棧眼耀,在符號化之前其實(shí)就是一堆UInt64的數(shù)據(jù),假設(shè)我們的堆棧一共有10行信息佩憾,那么實(shí)際上就是10個(gè)UInt64數(shù)據(jù)哮伟,共640字節(jié)。但是實(shí)際上iOS中一個(gè)指針UInt64根本用不上全部的64bit信息妄帘。因此在這里可以做個(gè)優(yōu)化楞黄,用堆棧距離(UInt64)&_mh_execute_header的偏移來替代堆棧,這樣既可用32bit信息來表示64bit信息抡驼。記錄堆棧所消耗的內(nèi)存減少接近一半鬼廓。

另外,還有個(gè)有趣的問題婶恼。大家看到的很多關(guān)于抓取堆棧的代碼中桑阶,保存棧幀的函數(shù)都是vm_read_overwrite,為什么不用memcpy或者像這樣直接用指針去操作呢勾邦?vm_read_overwrite非常慢,在dealloc中即使是灰度少量地使用割择,也絕對會卡爆你眷篇。那在dealloc中我們到底能不能用memcpy呢?

從這個(gè)問題我發(fā)現(xiàn)我對內(nèi)存機(jī)制不了解荔泳,感覺內(nèi)存這塊很有趣蕉饼。

簡單來說vm_read_overwrite是安全讀取地址的虐杯,具有探測機(jī)制,即使是個(gè)非法地址也能保證程序正常運(yùn)行昧港。但是memcpy則是簡單直接但是不安全擎椰。至于為什么大多數(shù)抓取堆棧都是使用vm_read_overwrite則是看中了vm_read_overwrite的安全能力,因?yàn)榭缇€程回溯堆棧并且沒有掛起所有線程创肥,可能會造成讀取非法地址的情況达舒。因此只要我們能保證當(dāng)前線程堆棧不會被破壞就可以用memcpy替代vm_read_overwrite,顯然我們這里是在當(dāng)前dealloc線程同步回溯叹侄,不會出現(xiàn)問題巩搏,因此可以用memcpy優(yōu)化調(diào)用。

線上效果

說了那么多趾代,這東西到底敢不敢上線使用贯底?使用后到底能不能發(fā)現(xiàn)問題?目前代碼已經(jīng)上線一段時(shí)間了撒强,線上放量30w用戶禽捆,總的來說還是能收集到一些問題的。例如下面的問題飘哨,根據(jù)捕捉到的信息排查后發(fā)現(xiàn)睦擂,是多線程使用不當(dāng)引起過度釋放。

監(jiān)控結(jié)果

抽象總結(jié)起來就是:

self.dic = [NSDictionary new];
for (int i = 0 ; i < 3000 ; i++) { 
dispatch_async(dispatch_get_global_queue(0, 0), ^{
        self.dic = @{@"name":@(i)};
    });
}

不足與改進(jìn)

一個(gè)技術(shù)方案不能只說優(yōu)點(diǎn)杖玲,還應(yīng)該給大家展示下相應(yīng)的缺點(diǎn)及不足顿仇。總的來說從現(xiàn)階段來說我感覺有3點(diǎn)不太滿意:

  • 內(nèi)存摆马、內(nèi)存臼闻、內(nèi)存!目前實(shí)際消耗的內(nèi)存要比我們記錄的消耗要偏大囤采。這是由于還有些我們調(diào)用的函數(shù)內(nèi)部會消耗一些內(nèi)存述呐,還沒有被發(fā)現(xiàn),這會導(dǎo)致極端情況下實(shí)際內(nèi)存在一直增長而內(nèi)存緩存遲遲得不到釋放蕉毯。典型的例子就是objc_setAssociatedObject乓搬,這個(gè)函數(shù)內(nèi)部維護(hù)了一個(gè)unordermap

  • 缺少相應(yīng)的控制平臺代虾。目前灰度都是服務(wù)端寫死進(jìn)行控制进肯,想要靈活控制非常不方便。我的想法是可以根據(jù)機(jī)型棉磨、系統(tǒng)江掩、版本等按百分比進(jìn)行控制灰度,并且可以靈活配置針對設(shè)備進(jìn)行分布式探測,例如總共8000個(gè)類环形,按設(shè)備占比分布到8種設(shè)備上策泣,這樣每種設(shè)備只承擔(dān)了1000個(gè)監(jiān)控任務(wù),壓力極大減少抬吟。這一步正在規(guī)劃中萨咕,要放假了,年后再說~

  • 缺少通用符號化平臺火本。目前我們上報(bào)的堆棧還沒有被符號化危队,需要本地進(jìn)行符號化,這對開發(fā)者有一定的要求发侵,使用不便交掏。規(guī)劃、放假刃鳄、年后說~

參考文獻(xiàn)

1盅弛、大白健康系統(tǒng)--iOS APP運(yùn)行時(shí)Crash自動修復(fù)系統(tǒng)

2、JJException

3叔锐、iOS 野指針定位:野指針嗅探器

4挪鹏、iOS野指針定位總結(jié)

5、淺談 iOS 中的 Crash 捕獲與防護(hù)

6愉烙、xiejunyi'Blog

7讨盒、Tagged Pointer對象安全氣墊為何會失效

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市步责,隨后出現(xiàn)的幾起案子返顺,更是在濱河造成了極大的恐慌,老刑警劉巖蔓肯,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件遂鹊,死亡現(xiàn)場離奇詭異,居然都是意外死亡蔗包,警方通過查閱死者的電腦和手機(jī)秉扑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來调限,“玉大人舟陆,你說我怎么就攤上這事〕馨” “怎么了秦躯?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長淘钟。 經(jīng)常有香客問我宦赠,道長陪毡,這世上最難降的妖魔是什么米母? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任勾扭,我火速辦了婚禮,結(jié)果婚禮上铁瞒,老公的妹妹穿的比我還像新娘妙色。我一直安慰自己,他們只是感情好慧耍,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布身辨。 她就那樣靜靜地躺著,像睡著了一般芍碧。 火紅的嫁衣襯著肌膚如雪煌珊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天泌豆,我揣著相機(jī)與錄音定庵,去河邊找鬼。 笑死踪危,一個(gè)胖子當(dāng)著我的面吹牛蔬浙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播贞远,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼畴博,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蓝仲?” 一聲冷哼從身側(cè)響起俱病,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎袱结,沒想到半個(gè)月后亮隙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡擎勘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年咱揍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棚饵。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡煤裙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出噪漾,到底是詐尸還是另有隱情硼砰,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布欣硼,位于F島的核電站题翰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜豹障,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一冯事、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧血公,春花似錦昵仅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至垦写,卻和暖如春吕世,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背梯投。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工命辖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人晚伙。 一個(gè)月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓吮龄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親咆疗。 傳聞我的和親對象是個(gè)殘疾皇子漓帚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容