這個月因為組內 iOS 工程師緊缺,所以臨時啃起了兩年多沒看的 ObjC 相關的內容,充當救火隊員,客串了一把 iOS 工程師。被指派的第一個任務是排查 App iOS 版本存在的嚴重的內存洩漏的問題,原因是 iOS 10 的某些系統 bug (參考文章:聊聊蘋果的Bug - iOS 10 nano_free Crash)導致線上出現了較多的 nano_free 和 nano_realloc 的 crash 問題,而這些 crash 會被觸發的根本原因則是 App 的內存洩漏問題沒有很好的治理。
iOS 內存洩漏的原因無非就是那麼幾個,跟 Android 非常相似:某些重型對象(Activity 或者 UIViewController)在該被釋放的時候未釋放,一直被其內部的對象所持有。排查內存洩漏的手段在 Android 上有 MAT 或者最新的 Android Studio,在 iOS 則上主要依賴 Xcode 提供的 Instruments 工具。但是眾所周知,等你知道存在內存洩漏再來排查就已經很晚了。而且這些工具雖然好用,但是真的排查起來還是相對比較困難的,因為很大的原因在於你並不清楚 App 到底在哪幾個頁面發生了洩漏!
在 Android 上,Square 這家公司提供了非常有名的工具:leakcanary,來幫助開發者們在日常開發過程中就能夠發現內存洩漏。但在 iOS 上呢?在 Google 的時候,我發現了兩個工具,一個是這篇文章將要翻譯並介紹的 Facebook 開源的三件套,另一個則是國內微信閱讀團隊做的?MLeaksFinder。
關於 MLeaksFinder 這裡有兩篇其官方提供的文章介紹:
MLeaksFinder:精准 iOS 內存洩露檢測工具
MLeaksFinder 新特性
簡而言之,MLeaksFinder 使用了一個非常 tricky 的方法來檢測內存洩漏:通常一個 UIViewController 在被 pop 之後將會很快被釋放,假設在 pop 3 秒鐘之後仍然沒有被釋放,則可以認為這個 UIViewController 存在洩漏的問題(其實類似的方案同樣也可以在 Android 上實現,據我所知微信在早期就有類似原理實現的用在 Android 上的內存洩漏監測工具了)。在後續的更新版本中,MLeaksFinder 也依賴了 Facebook 的?FBRetainCycleDetector?來輔助判斷內存洩漏是否是由循環引用引起的。
MLeaksFinder 的原理非常簡單但有效,幫助我排查了 App 中存在的不少內存洩漏問題,而且對於整個應用基本零侵入,不需要做任何的配置與修改。不過缺點也在這裡,因為如果要將其作為日常自動化的工具使用的話,我希望 MLeaksFinder 本身可以提供回調接口,以便在內存洩漏發生時我可以選擇是彈出 Alert 提示開發者,還是通過後台上報的方式提交。當然,開源的好處在於可以修改源碼滿足自己的需求,後續也會向其提交 PR,完善這個非常精巧的項目。
Facebook 的工程師們其實早就已經將 iOS 的內存洩漏排查自動化了,並發布了一篇非常不錯的文章來介紹其原理,以及開源了他們的三個工具套件。為了加深對 Objective-C 內存管理的理解以及對庫的原理的了解,在文末,會有我對這全篇英文文章的翻譯。
對於工程師而言,自動化的工具真的是排名前幾的生產力。
如果不想看翻譯的話,這裡也提供一個大概的全文重點概覽:
三個開源工具:
1.FBRetainCycleDetector
主要用於檢測循環引用
2.FBAllocationTracker
主要用於快速檢測潛在的內存洩漏對象,並提供給 FBRetainCycleDetector 進行檢測
3.FBMemoryProfiler
可視化工具,直接嵌入到 App 中,可以起到在 App 中直接查看內存使用情況,並篩選潛在洩漏對象的作用
Facebook 的自動化:客戶端自動監測 -> 上報服務端 -> 歸類/篩選 -> 分發給指定人員 -> 處理內存洩漏
未開源的部分在於服務端如何對上報的循環引用鏈進行歸類與篩選,不過 Facebook 的工程師們在文中給出了他們自己的策略
拓展閱讀
最後放一些相關的知識點的拓展閱讀,有助於理解全文內容。
Objective-C Class Ivar Layout 探索
Type Encodings
P.S.?Sunny 老師(滴滴?DynamicCocoa?的作者)對於 OC Runtime 玩得真是溜,看他的博客非常長見識…
P.P.S.?螞蟻金服財富事業群還在招聘 Android / iOS 工程師,如果你感興趣的話歡迎給我發來你的簡歷:yong.hy[AT]alipay.com
原文鏈接 [需翻牆]:Automatic memory leak detection on iOS
譯文
內存是移動設備上的共享資源,如果一個 App 無法正確地進行內存管理的話,將會導致內存消耗殆盡,閃退以及性能的嚴重下降。
Facebook 的 iOS 版本的許多功能模塊共用了同一份內存空間,如果其中的某一個模塊消耗了特別多的內存資源的話,將會對整個 App 造成嚴重影響。舉個栗子,當某個功能模塊不小心造成了內存洩漏的時候,這個情況就很有可能會發生。
在 Facebook,我們有非常多的工程師同時在一個代碼倉庫下進行並行開發。內存洩漏是在開發過程中難以避免會遇見的問題。當內存洩漏發生時,我們就需要快速地去發現然後修復它。
現在已經存在一些開發者工具來輔助發現內存洩漏了,但是它們的共同點是需要大量的人工操作:
打開 Xcode 並選擇 build for profiling 來編譯你的工程
打開 Instruments 工具
嘗試在你的應用上盡可能多地重現更多的場景與行為
觀察內存工具的走勢圖
找到內存洩漏的源頭
修復它!
這樣的人工排查與修復工程每次都得不斷地重復操作。正因為如此,我們很難在迭代階段早期就定位與修復內存問題。
將內存洩漏的排查過程盡可能地自動化,減少開發人員的人工干預,可以幫助我們更快地去找到內存洩漏的地方。為了解決這個問題,我們已經在內部開發了一套工具來幫助我們自動化這個排查過程,並且已經幫助我們解決了許多代碼中存在的內存洩漏問題。今天,我們很高興向大家宣布我們正式開源這套內存洩漏排查工具:FBRetainCycleDetector,FBAllocationTracker?和?FBMemoryProfiler。
循環引用
Objective-C 使用引用計數來管理內存與釋放未被引用的對象。內存中的對象 A 可以讓對象 B 的引用計數加一,即 retain,來使對象 B 盡可能久地存在內存中(只要對象 A 不對它“減一”,即 release)。也就是說:對象 A 持有了對象 B 。
大多數情況下,引用計數這套機制都可以運作得很好。但是,當兩個對象直接地,或者更常見的情形是通過某些對象間接地,互相持有了對方,這個時候就陷入了僵局了。這種互相持有對方的引用的現象叫做循環引用。
循環引用會導致一系列的問題。最好的情況是,洩漏的對象本身就會一直長期地占用內存空間,這種情況一般不會造成太大的內存消耗。如果洩漏的對象不停地增加與積累,那麼 App 中其他功能模塊所能使用的內存就會減少。最壞的情況則是,內存洩漏導致了 App 需要使用的內存超出了限制,這時應用就會閃退了。
通過人工排查的手段,我們發現我們有太多因為循環引用導致的內存洩漏了。日常編碼中,稍不加注意就有可能把循環引用給引入到代碼裡,而之後卻不容易發現他們。FBRetainCycleDetector?這個工具將幫助我們把循環引用的監測變得更加簡單。
在運行期監測循環引用
在 Objective-C 中檢測循環引用可以抽象為在一個節點為對象,邊為對象之間的引用關系的有向無環圖(DAG 圖)中尋找存在的環。當所有的 Objective-C 對象已經在我們的有向無環圖中時,我們所需要做的就是通過深度優先搜索算法來遍歷它,並找到循環節點。
這裡有個視頻 - 需翻牆
將循環引用的檢測問題抽象為簡單的數據結構算法之後,整個方案就變得非常清晰了。我們需要確認的就是我們能夠在運行期找到所有的內存對象並找出他們之間所有的引用關系。對象之間的引用關系可能是弱引用,也可能是強引用。而只有對象之間的強引用才會導致循環引用。因此,我們只需要找到每個對象所存在的強引用即可。
幸運地是,Objective-C 提供給了我們非常強大的 Runtime 庫,可以幫助我們在運行期獲取足夠的數據來構建這樣一張有向無環圖。
有向無環圖中的節點可以是一個對象,或者是一個 block,接下來我們將分別進行討論。
Objects(對象)
Objective-C Runtime 提供了很多工具來幫助我們在運行期獲取一個對象的詳細信息(也稱作內省,Introspection,這是面向對象語言和環境的一個強大特性)。
我們要做的第一件事情就是獲取對象中所有變量的 ivar layout。
? ?
對於一個給定的對象,它的 ivar layout 可以讓我們獲取到這個對象持有了多少對別的對象的引用關系。ivar layout 為我們提供了一個內存地址偏移量的“索引”,讓我們能夠通過對“索引”的疊加來得到它所持有的另一個對象的內存地址。OC Runtime 也給我們提供了可以獲取一個對象的所有的弱引用關系的工具:weak ivar layout。我們可以假定: ivar layout 與 weak ivar layout 之間的關系鏈條差值即一個對象的所有強引用關系。
除此之外,還需要額外做一部分工作來支持 Objective-C++。在 Objective-C++ 中,我們可以在 structs 中定義對象,而這樣的對象不會被 ivar layout 提供索引。而 OC Runtime 剛好提供了類型編碼機制(Type Encoding)來幫助我們處理這個問題。對於每一個實例變量,類型編碼可以告訴我們這個對象是什麼樣的數據類型。如果對象是個 struct,那麼類型編碼會告訴我們這個 struct 是由哪些字段和類型組成的。我們通過轉換類型編碼來發現哪些實例變量是 Objective-C 的對象,並計算內存地址的偏移量來獲取所有他們指向的對象地址。
不過仍然有一些邊界 case 我們不能深入地去解決。大多數是跟一些集合類型相關的,我們不得不遍歷集合來獲取所有的變量所持有的對象,這可能會導致一些潛在的副作用。
Blocks
Blocks 跟對象有一些不同。OC Runtime 沒有提供給我們簡單的獲取它的 ivar layout 的方法,但是我們可以通過一些小 trick 來解決這個問題。
我們借鑒了 Mike Ash 在他的項目?Circle?中使用的方法來處理 blocks,這個方案也啟發了我們開發了 FBRetainCycleDetector。
這裡我們運用到了 ABI (application binary interface for blocks)。它告訴我們 block 在內存中是以怎樣的形式存在的。如果我們知道我們在處理的引用是一個 block 的話,那麼我們可以將其轉化為一個假的模擬 block 的 struct 對象。在將 block 轉化為 struct 之後,我們就能夠知道它所持有的內存對象了。但是不幸地是,我們並不知道這些引用關系是強引用還是弱引用。
我們用了黑盒技術來解決這個問題。我們創建了一個假裝是我們要排查的 block 的對象,且我們知道 block 的接口結構,以及到哪去找 block 所持有的引用。而在我們“偽造的” block 對象中,並沒有實際持有引用,而是持有了“釋放探測器”(release detectors)。釋放探測器是一些用來監聽發送給他們的內存釋放消息的小對象。當一個對象要解除引用關系的時候,它會像其持有的強引用對象發送內存釋放的消息。所以,我們可以在釋放“偽造的” block 對象的時候,檢查一下哪個探測器收到了內存釋放的消息,這些收到消息的探測器,即存在強引用關系,這樣就可以幫助我們找出真實的 block 對象中所持有的對象引用了。
自動化
這個內存檢測工具在我們日常持續不斷地迭代構建開發中,不停地發光發熱貢獻著自己的一份力。
在客戶端上的自動化非常地簡單。我們在自己的 App 裡添加了 Retain Cycle Detector 的依賴,並周期性地掃描內存片段來檢測循環引用。當然,它也並不是完璧無瑕的。當我們第一次運行 Retain Cycle Detector 的時候,我們就意識到了它是無法非常快地掃描整個內存使用的,我們需要為它提供一些篩選過後的內存對象來讓它進行檢測。
為了更有效地進行對象篩選,我們開發了 FBAllocationTracker。這是一個用來主動追蹤所有 NSObject 的子類的內存分配和釋放操作的工具。它可以在最小的性能消耗下,在給定的時間點快速獲取任何類的任何實例。
在客戶端上自動進行內存洩漏監測實際上就是配合使用 FBRetainCycleDetector 加定時器,再加上可以為我們篩選檢測對象的 FBAllocationTracker。
現在讓我們來看下後端需要做哪些特別的處理。
循環引用可以由任意數量的多個對象組成。但是當出現了一個壞鏈接而導致出現多個循環時,事情就變得復雜起來了。
A→B is a bad link in a cycle, and two kinds of cycles are created because of that: A-B-C-D and A-B-C-E.
這會導致兩個問題:
如果是因為一個壞鏈接導致的兩個循環引用,我們並不想把他們分開標記出來
如果真的是兩個循環引用問題,即便是共享了一個鏈接,我們也不想把他們標記在一起
因此,我們需要對循環引用進行歸類,為此我們也寫了一個算法來啟發式地幫助我們處理這個問題:
將同一天檢測到的循環引用歸類
對每一個循環引用,提取出 Facebook 特有的類名
對每一個循環引用,找到其中已經被上報過的、並包含在其中的最小環
將每個循環引用添加到上述最小環所表示的組中
只對最小環進行上報
做完上述的處理之後,最後要做的事情就是找到哪個工程師改動了代碼導致了內存洩漏的發生。我們通過 ‘git/hg blame’ 配合循環引用中的部分代碼,來推測出可能是某個工程師的改動導致的問題發生,並在內部的辦公系統中對其發出通知,讓他盡快修復這個問題。
整個自動化系統可以通過下面的圖形進行表示:
人工檢測
雖然自動化的方式幫助我們簡化了循環引用的排查,也減輕了我們工程師的負擔,但是人工排查仍然具有不可替代的作用。為此,我們還開發了 FBMemoryProfiler 來讓任何人都可以在不把 iPhone 連接到電腦的情況下,也可以當前 App 的內存使用情況。
FBMemoryProfiler 可以通過很簡單的方式就添加到任何 App 中,讓你具備直接在 App 中查看內存使用情況,並手動檢查循環引用的能力。FBMemoryProfiler 依賴了 FBAllocationTracker 和 FBRetainCycleDetector 來實現這些功能。
這裡有個視頻 - 需翻牆
Generations(代)
FBMemoryProfiler 提供的另一個非常好用的功能是“代追蹤”(generation tracking),類似於 Xcode 提供的 Instruments 裡的代追蹤的功能。“代”(Generation)可以認為是兩個時間切片之間所有存在的內存對象的快照(snapshots)。
舉個栗子,通過 FBMemoryProfiler 提供的 UI 你標記了一次時間點,這時分配了三個內存對象。然後你標記了另一個時間點,同時也繼續分配內存對象。第一個時間點的內存中只包含了我們最開始的三個內存對象。如果其中任何一個對象被釋放了,那麼它就不會存在第二個時間點的內存切片中。
當我們需要做一些重復的動作的時候,代追蹤這個功能是非常有用的。舉個栗子,比如我們需要反復進出某個 View Controller 的時候。我們每次都在進入 View Controller 之前標記一下內存快照,然後仔細關注一下每次內存快照都有哪些對象剩下。如果有個對象存在的時間超出了它原本的預期的話,我們就可以非常直觀地從 FBMemoryProfiler 上看到。
來試試看吧
不論你是巨無霸 App 還是一個小型應用,良好的內存管理都是一個好的工程習慣。通過這些工具的幫助,我們能夠更為便捷地去發現和修復內存洩漏的問題,讓我們省下那些去手動檢測的時間,更加聚焦在寫出更好的代碼上。我們也希望你能夠從這些工具裡得到幫助。來 Github 上給我們加個 star 吧:FBRetainCycleDetector,FBAllocationTracker?和?FBMemoryProfiler。