FBRetainCycleDetector遇到NSMapTable的crash - 從發(fā)現(xiàn)到PR

自從項目接入了 MLeaksFinder + FBRetainCycleDetector 的內(nèi)存泄漏檢測方案切距,在收獲了許多有效內(nèi)存泄漏的同時,我們也收獲了兩個 FBRetainCycleDetector 的 crash丢胚。

首先拋出這兩個 crash 的調(diào)用棧:

問題1:

Crashed: com.mapp.cycleDetector
0  libobjc.A.dylib                0x1903be058 objc_retain + 8
1  MAppInHouse                    0x10594e1ac FBWrapObjectGraphElement + 64 (FBRetainCycleUtils.m:64)
2  MAppInHouse                    0x10594c324 -[FBObjectiveCObject allRetainedObjects] + 83 (FBObjectiveCObject.m:83)
3  MAppInHouse                    0x10594a868 -[FBNodeEnumerator nextObject] + 34 (FBNodeEnumerator.mm:34)
4  MAppInHouse                    0x10594d0a8 -[FBRetainCycleDetector _findRetainCyclesInObject:stackDepth:] + 132 (FBRetainCycleDetector.mm:132)
5  MAppInHouse                    0x10594caac -[FBRetainCycleDetector findRetainCyclesWithMaxCycleLength:] + 65 (FBRetainCycleDetector.mm:65)
6  MAppInHouse                    0x1061c4174 __55-[NSObject(MemoryLeak) checkRetainCycleWithCompletion:]_block_invoke.165 + 256 (NSObject+MemoryLeak.m:256)
7  libdispatch.dylib              0x190348678 _dispatch_call_block_and_release + 24
8  libdispatch.dylib              0x1903491ec _dispatch_client_callout + 16
9  libdispatch.dylib              0x19032675c _dispatch_lane_serial_drain$VARIANT$armv81 + 564
10 libdispatch.dylib              0x190327178 _dispatch_lane_invoke$VARIANT$armv81 + 404
11 libdispatch.dylib              0x1903304bc _dispatch_workloop_worker_thread + 576
12 libsystem_pthread.dylib        0x190398f5c _pthread_wqthread + 304
13 libsystem_pthread.dylib        0x19039baa0 start_wqthread + 8

問題2:

Crashed: com.mapp.cycleDetector
0  CoreFoundation                 0x21f4c37e0 ___forwarding___ + 1448
1  CoreFoundation                 0x21f4c546c _CF_forwarding_prep_0 + 92
2  MAppInHouse                    0x1018e70d4 FBWrapObjectGraphElementWithContext + 43 (FBRetainCycleUtils.m:43)
3  MAppInHouse                    0x1018e72f4 FBWrapObjectGraphElement + 65 (FBRetainCycleUtils.m:65)
4  MAppInHouse                    0x1018e5454 -[FBObjectiveCObject allRetainedObjects] + 83 (FBObjectiveCObject.m:83)
5  MAppInHouse                    0x1018e3998 -[FBNodeEnumerator nextObject] + 34 (FBNodeEnumerator.mm:34)
6  MAppInHouse                    0x1018e61d8 -[FBRetainCycleDetector _findRetainCyclesInObject:stackDepth:] + 132 (FBRetainCycleDetector.mm:132)
7  MAppInHouse                    0x1018e5bdc -[FBRetainCycleDetector findRetainCyclesWithMaxCycleLength:] + 65 (FBRetainCycleDetector.mm:65)
8  MAppInHouse                    0x10215d2a4 __55-[NSObject(MemoryLeak) checkRetainCycleWithCompletion:]_block_invoke.165 + 256 (NSObject+MemoryLeak.m:256)
9  libdispatch.dylib              0x21eef56c8 _dispatch_call_block_and_release + 24
10 libdispatch.dylib              0x21eef6484 _dispatch_client_callout + 16
11 libdispatch.dylib              0x21eed0fa0 _dispatch_lane_serial_drain$VARIANT$armv81 + 548
12 libdispatch.dylib              0x21eed1ae4 _dispatch_lane_invoke$VARIANT$armv81 + 412
13 libdispatch.dylib              0x21eed9f04 _dispatch_workloop_worker_thread + 584
14 libsystem_pthread.dylib        0x21f0d90dc _pthread_wqthread + 312
15 libsystem_pthread.dylib        0x21f0dbcec start_wqthread + 4

FBRetainCycleDetector 是 facebook 出品的尋找循環(huán)引用的工具。簡單來說,它通過class_copyIvarList獲取一個類的實例變量列表乖订,使用class_getIvarLayout判定是實例變量是否為強引用终息,然后使用有向圖中找環(huán)的算法夺巩,獲取循環(huán)引用的引用環(huán)。

光從調(diào)用棧上來看周崭,我們對這一問題沒有頭緒柳譬。首先,這兩個 crash 并非必現(xiàn)续镇;其次美澳,從崩潰用戶的行為上看,也沒有發(fā)現(xiàn)共性。

作為一個 facebook 出品制跟,經(jīng)過了多年驗證的三方庫柴墩,我們判斷這兩個 crash 并非一般的代碼邏輯問題。解決這兩個問題看起來會是一個挑戰(zhàn)凫岖。

錯誤的判斷

一開始我們以為問題 1 是一個多線程的問題江咳,因為 FBRetainCycleDetector 有一段注釋,表明它的確可能存在多線程問題哥放,只是用 try catch 嘗試縮小它的影響歼指。

同時,問題 1 的調(diào)用棧中甥雕,的確有多個線程在進行找環(huán)操作踩身。

我們曾嘗試通過將并發(fā)隊列改為串行隊列的方式修復(fù)問題1,但是并未修好社露。

線索

來自 github issue

遇到疑難問題挟阻,特別是開源庫的問題,我們迅速反應(yīng)出峭弟,去網(wǎng)絡(luò)上嘗試尋找解決方案附鸽。

https://github.com/facebook/FBRetainCycleDetector/issues/60#issuecomment-503511056

從 FBRetainCycleDetector 的 github issue 上,我們發(fā)現(xiàn)了一個與問題1類似的問題描述瞒瘸。其中坷备,提問者提到,這是遍歷 NSMapTable 時遇到的情臭。

NSMapTable 是我們獲得的第一個線索省撑。

一次偶然的復(fù)現(xiàn)

同時,我們在調(diào)試時俯在,也偶然復(fù)現(xiàn)了一次問題2竟秫。這次復(fù)現(xiàn)給了我們關(guān)鍵的信息。

當(dāng)時的現(xiàn)場是跷乐,正在找環(huán)過程中的object對象變成了一個指向0xffffffffffffffff地址的指針肥败,而這個指針通過object_getClass竟然能取到對應(yīng)的類,對應(yīng)的類是__NSAtom劈猿。

__NSAtom顯然是一個私有類拙吉,而且它不繼承自NSObject,沒有isSubclassOfClass:方法揪荣,所以執(zhí)行到這里的時候筷黔,觸發(fā)消息轉(zhuǎn)發(fā)最后EXC_BREAKPOINT了。

此時仗颈,我們想到了一個最簡單的修復(fù)方式:在這里繞過isSubclassOfClass:方法佛舱,使用 runtime 的 API class_getSuperclass 來達到判斷是否是子類的目的椎例。

但是,不查明這個0xffffffffffffffff的由來请祖,只修復(fù)問題的表面订歪,也讓我們心虛。0xffffffffffffffff顯然是一個不符合預(yù)期的地址肆捕,而隨意訪問這種地址刷晋,可能會引爆更大的雷。

所以慎陵,我們不得不對這個問題做更多分析眼虱。

穩(wěn)定復(fù)現(xiàn)

剛才的線索中,我們得到了兩個重要信息:

  1. NSMapTable 是問題的來源
  2. 一個莫名其妙的數(shù)被當(dāng)成了對象的地址

已知的是席纽,NSMapTable 作為一個功能更強大的容器捏悬,不僅僅可以存放對象,還能存放一個簡單的數(shù)字润梯。所以过牙,我們嘗試用 NSMapTable 來穩(wěn)定復(fù)現(xiàn)問題2。

復(fù)現(xiàn)的方式其實很簡單纺铭。

創(chuàng)建一個NSPointerFunctionsOpaqueMemory類型的容器寇钉,往容器里塞入 -1 這個數(shù),也就是 0xffffffffffffffff彤蔽,然后讓這個容器被找環(huán)算法遍歷到摧莽。

xsqView.table = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsOpaqueMemory | NSPointerFunctionsIntegerPersonality valueOptions:0 capacity:0];
NSInteger i = -1;
[xsqView.table setObject:@"hahaha" forKey:(__bridge id)((void *)i)];

問題2被復(fù)現(xiàn)了出來庙洼。

而將這個數(shù)從 -1 改到 1顿痪,我們發(fā)現(xiàn)問題1也成了必現(xiàn)。

問題1和問題2油够,預(yù)期是同一個本質(zhì)問題引起的蚁袭。

分析問題1

穩(wěn)定復(fù)現(xiàn)后,問題1的分析變得順利了起來石咬。

數(shù)字 “1” 被 FBRetainCycleDetector 遍歷到的時候揩悄,F(xiàn)BRetainCycleDetector 使用了__strong 的 id 類型修飾它,導(dǎo)致運行時被調(diào)用了 _objc_retain鬼悠,因此導(dǎo)致了 BAD ACCESS删性。

如果將這里的 id ,和 FBWrapObjectGraphElement 函數(shù)參數(shù)中的 id焕窝,都修改為 __unsafe_unretained id蹬挺,這個 crash 堆棧立馬變到了下一處對數(shù)字 “1” 進行強引用的地方。

所以問題1的本質(zhì)原因被找到且證明了它掂。FBRetainCycleDetector 并沒有考慮到 NSPointerFunctionsOpaqueMemory 類型的容器巴帮,將容器內(nèi)的元素都當(dāng)作了對象來對待導(dǎo)致了問題1。

分析問題2

問題1的分析比較容易。但為什么將數(shù)字 "1" 改成 "-1" 后榕茧,問題1中 BAD ACCESS 的代碼被順利走過了垃沦,crash 堆棧變成了問題2呢?

搜索了一些資料用押,發(fā)現(xiàn)這是 tagged pointer 搞的鬼肢簿。

簡單說,計算機中有內(nèi)存對齊的說法蜻拨,因此正常的指針译仗,在 64 位設(shè)備上,最后 4 bit 必然是0官觅。如果最后 4 bit 不是 0纵菌,說明這不是一個正常的指針。這個特性被蘋果用于了tagged pointer休涤。

http://www.phrack.org/issues/69/9.html 我從這篇博客里了解了一下tagged pointer)

由于一個對象的結(jié)構(gòu)體中的第一個成員是 isa 指針咱圆,因此,如果 0xffffffffffffffff 被當(dāng)作了一個對象功氨,那么它實際也被當(dāng)作了一個 isa 指針的值序苏。而顯然,這個 isa 指針還是個 tagged pointer捷凄。

如果一個 isa 指針是一個 tagged pointer 的話忱详,它找到的 Class 的過程中會經(jīng)歷一個映射。經(jīng)過映射跺涤,它最后可以被翻譯為某一個屬于 TaggerPointer 的類匈睁,比如 __NSAtom。所以桶错,就出現(xiàn)了問題 2 中的崩潰棧航唆。

(我們可以從開源的runtime代碼中了解映射的過程https://opensource.apple.com/source/objc4/objc4-551.1/runtime/objc-private.h

問題根源

其實,通過分析 FBRetainCycleDetector 的找環(huán)邏輯院刁,我們會發(fā)現(xiàn)糯钙,這些 “數(shù)字” 本來就不應(yīng)該被遍歷到。

因為存儲了 NSPointerFunctionsOpaqueMemory 元素的容器退腥,容器持有容器內(nèi)元素的關(guān)系任岸,并不是強引用。

FBRetainCycleDetector 其實也考慮到了這點狡刘,它有一個方法來判斷容器是不是強引用:


但是對于 NSPointerFunctionsOpaqueMemory 的容器享潜,usesWeakReadAndWriteBarriers 屬性返回的是 NO,所以被誤判成了強引用颓帝。

解決

NSPointerFunctions 沒有開放接口判斷它的 option 是什么米碰∥迅铮看起來我們無法分辨出 NSPointerFunctions 與元素的引用關(guān)系。但是在分析了 NSPointerFunctions 的接口文檔后吕座,我們發(fā)現(xiàn)了一個 trick 但合理的方案虐译,就是利用它的 acquireFunction 屬性。

官方文檔是這樣描述 acquireFunction 屬性的吴趴。

The function used to acquire memory.

This specifies the function to use for copy-in operations.

這個屬性是一個函數(shù)指針漆诽,當(dāng)一個值被存入容器時,會調(diào)用這個函數(shù)锣枝,按需去 retain 這個即將被存入容器的元素厢拭。

我們做了個實驗了。如果 option 是 NSPointerFunctionsStrongMemory撇叁,則這個 acquireFunction 是系統(tǒng)提供的函數(shù)供鸠,如果 option 是 NSPointerFunctionsOpaqueMemory,這個 acquireFunction 是空陨闹。

這個結(jié)果很好理解楞捂,也符合正常程序員的設(shè)計思路,當(dāng)容器不想對存入的值做內(nèi)存上的操作趋厉,什么也不干就行了寨闹。

所以我們可以推斷,如果 acquireFunction 為空君账,說明這個容器并不會對元素的引用計數(shù)去 +1繁堡,這說明對元素的引用關(guān)系,并非是強引用乡数。

當(dāng)然這個論斷反過來并不能推定椭蹄。

所以我們可以在 FBRetainCycleDetector 的邏輯里加一個判斷:

當(dāng)容器的 NSPointerFunctions 的 acquireFunction 為空時,至少能說明它不會強引用存儲的元素瞳脓∷芙浚可以直接放棄遍歷其內(nèi)部的元素。

驗證

我們已經(jīng)通過獲取 acquireFunction 達成了如上推斷劫侧,為了進一步驗證,我們用 Hopper 查看了逆向出來的偽代碼哨啃,發(fā)現(xiàn)至少在 iOS 12.3.1 上烧栋,我們的推測是正確的。

我們不能保證 NSPointerFunctionsOpaqueMemory 的容器的 acquireFunction 在任何版本的 iOS 上都是空拳球,但是增加對 acquireFunction 的判斷好過什么也不做审姓。

提交

這個修復(fù)被首先提交到了項目中進行驗證。證明修復(fù)有效后祝峻,我們給開源的 FBRetainCycleDetector 提交了同樣的修復(fù):

https://github.com/facebook/FBRetainCycleDetector/pull/79

同時在 FBRetainCycleDetector 的單元測試里增加了必現(xiàn)問題1的case魔吐。

總結(jié)

在這個問題發(fā)現(xiàn)的初期扎筒,我抱著絕望的態(tài)度,認為開源庫中的 crash 必然難解酬姆。但是事實證明嗜桌,通過收集線索、耐心分析問題辞色、制造必現(xiàn)場景骨宠、理解 root cause、大膽假設(shè)小心驗證相满,問題依然是有機會解決的层亿。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市立美,隨后出現(xiàn)的幾起案子匿又,更是在濱河造成了極大的恐慌,老刑警劉巖建蹄,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件琳省,死亡現(xiàn)場離奇詭異,居然都是意外死亡躲撰,警方通過查閱死者的電腦和手機针贬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拢蛋,“玉大人桦他,你說我怎么就攤上這事∽焕猓” “怎么了快压?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長垃瞧。 經(jīng)常有香客問我蔫劣,道長,這世上最難降的妖魔是什么个从? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任脉幢,我火速辦了婚禮,結(jié)果婚禮上嗦锐,老公的妹妹穿的比我還像新娘嫌松。我一直安慰自己,他們只是感情好奕污,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布萎羔。 她就那樣靜靜地躺著,像睡著了一般碳默。 火紅的嫁衣襯著肌膚如雪贾陷。 梳的紋絲不亂的頭發(fā)上缘眶,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音髓废,去河邊找鬼巷懈。 笑死,一個胖子當(dāng)著我的面吹牛瓦哎,可吹牛的內(nèi)容都是我干的砸喻。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼蒋譬,長吁一口氣:“原來是場噩夢啊……” “哼割岛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起犯助,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤癣漆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后剂买,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惠爽,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年瞬哼,在試婚紗的時候發(fā)現(xiàn)自己被綠了婚肆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡坐慰,死狀恐怖较性,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情结胀,我是刑警寧澤赞咙,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站糟港,受9級特大地震影響攀操,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜秸抚,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一速和、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧耸别,春花似錦健芭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽若贮。三九已至省有,卻和暖如春痒留,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蠢沿。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工伸头, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人舷蟀。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓恤磷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親野宜。 傳聞我的和親對象是個殘疾皇子扫步,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

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

  • 版權(quán)聲明本文轉(zhuǎn)自網(wǎng)易杭州前端技術(shù)部公眾號,由作者授權(quán)發(fā)布匈子。 前言 大白(Baymax)河胎,迪士尼動畫《超能陸戰(zhàn)隊》中...
    XueYongWei閱讀 2,017評論 2 11
  • 卷首語 歡迎來到 objc.io 第七期! 這個月虎敦,我們選擇了 Foundation 框架作為我們的主題游岳。 Fou...
    評評分分閱讀 1,517評論 0 8
  • 本文基于objc4-709源碼進行分析。關(guān)于源碼編譯:objc - 編譯Runtime源碼objc4-706 ob...
    WeiHing閱讀 809評論 1 3
  • 最近吳大叔和小三鬧得全網(wǎng)沸沸揚揚其徙,前段時間陳羽凡吸毒出軌胚迫,著名主持人朱軍也被舉報性騷擾而在打官司,雖還未下...
    Miya姑娘閱讀 156評論 0 0
  • 每次寫007的文章都很倉促唾那,所以文章質(zhì)量其實并不高访锻。把過去的文字一一看過,覺得即使這樣通贞,一個機制下自己能堅持地記錄...
    金笛Jindi閱讀 123評論 0 2