[譯文]在iOS上自動檢測內存泄露

博文連接:http://ifujun.com/yi-wen-zai-iosshang-zi-dong-jian-ce-nei-cun-xie-lu/
原文連接:https://code.facebook.com/posts/583946315094347/automatic-memory-leak-detection-on-ios/html

手機設備的內存是一個共享資源。應用程序可能會不當的耗盡內存、崩潰,或者遭遇大幅度的性能下降。ios

Facebook iOS客戶端有不少功能,而且它們共享同一塊內存空間。若是任何特定的功能消耗過多的內存,就會影響到整個應用程序。這是可能發生的,好比,這個功能致使了內存泄露。git

當咱們分配了一塊內存,並設置了對象以後,若是在使用完了以後忘記釋放,這就會發生內存泄露。這意味着系統是沒法回收內存並交予他人使用,這也最終意味着咱們的內存將會逐漸耗盡。github

在Facebook,咱們有不少工程師在代碼庫的不一樣部分上工做。這不可避免的會發生內存泄露。當發生內存泄露以後,咱們須要儘快找到並修復它們。算法

一些工具已經能夠找到內存泄露,可是它們須要大量的人工干預:segmentfault

  1. 打開Xcode,給性能分析(profiling)編譯。app

  2. 載入Instruments。ssh

  3. 使用應用程序,嘗試儘量多的重現場景和行爲。ide

  4. 查看內存和泄露。工具

  5. 追蹤內存泄露的根源。

  6. 修復這個問題。

這意味着每次都須要重複大量的手動操做。爲此,在咱們的開發週期上,咱們可能沒法儘量早的定位和修復內存泄露問題。

自動化能夠在不須要更多開發者的狀況下,更快的找到內存泄露。爲了解決這個問題,咱們作了一套工具來自動化的處理和修復咱們代碼庫中的一些問題。今天,咱們很興奮的發佈這些工具:FBRetainCycleDetectorFBAllocationTrackerFBMemoryProfiler

循環引用(Retain cycles)

Objective-C 使用引用計數去管理內存和釋放不使用的對象。內存中的任何一個對象均可以持有(retain)其餘的對象,只要前面的對象須要它,對象就會一直保持在內存中。查看這個的一個方法是這個對象持有其餘的對象。

在大部分時間內,這都工做的很好,但當兩個對象互相持有的時候,這就會陷入一個僵局。直接,或者更常見的,經過間接對象鏈接它們。這種持有引用的環咱們叫作循環引用(Retain cycles)。

循環引用會致使一些列的問題。最好的狀況下,對象只會在內存中佔有一點點位置。若是這個被泄露的對象正積極地作個一些不平凡的事情,應用程序的其餘部分就只會有更少的內存。最壞的狀況下,若是泄露致使使用超出可用內存的容量,那麼,應用程序會崩潰。

在手動性能分析期間,咱們發現,咱們每每有一些循環引用。咱們很容易引發內存泄露,可是很難找到它們。循環引用檢測器能夠很容易的找到它們。

在運行時檢測循環引用

在 Objective-C 中找循環引用相似於在一個有向無環圖(directed acyclic graph)中找環, 而節點就是對象,邊就是對象之間的引用(若是對象A持有對象B,那麼,A到B之間就存在着引用)。咱們的 Objective-C 對象已經在咱們的圖中,咱們要作的就是用深度優先搜索遍歷它。

segmentfault不支持嵌入視頻,建議跳轉到博客去瀏覽文章。
http://7i7i81.com1.z0.glb.clouddn.com/blogvideo_memoryleak_1.mp4

這有點抽象,但效果很好。咱們必須確保咱們能夠像節點同樣使用對象,對於每一個對象,咱們均可以獲取到它引用的全部對象。這些引用多是weak,也多是strong。只有強引用纔會致使循環引用。對於每一個對象來講,咱們須要知道如何找出這些引用。

幸運的是,Objective-C提供了一個強有力的、內省的運行時庫。這讓咱們在圖中能夠有足夠的數據去挖掘。

圖中的節點能夠是對象,也能夠是Block。讓咱們來分別討論一下。

對象

運行時有不少工具容許咱們對對象進行內省。

咱們要作的第一件事是獲取對象的實例變量的佈局(ivar layout)。

const char *class_getIvarLayout(Class cls);
const char *class_getWeakIvarLayout(Class cls);

對於對象,實例變量的佈局描述了咱們在哪兒能夠找到其餘對象的引用。它會提供給咱們一個索引(index),這表明咱們須要在對象地址上添加一個偏移量(offset),就能夠獲得它所引用的對象的地址。運行時也容許咱們獲取「弱引用實例變量佈局(weak ivar layout)」。

這也部分支持Objective-C++。在Objective-C++中,咱們能夠在結構體中定義對象,可是這不會在實例變量佈局中獲取到。運行時提供了「類型編碼(type encoding)」來處理這個問題。對於每個實例變量來講,類型編碼描述了變量是如何結構化的。若是這是一個結構體,它會描述它包含了哪些字段和類型。咱們計算出它們的偏移量,在圖中,找出它們所指向的對象。

也有一些邊緣條件咱們不會深刻。大部分是一些不一樣的集合,咱們不得不列舉它們去得到它們持有的對象,這可能會致使一些反作用。

Block

Block和對象有一點不同。運行時不會讓咱們很輕易的看到它們的佈局,可是咱們仍然能夠猜想。

在處理Block的時候,咱們可使用 Mike Ash 在他的項目Circle(第一時間啓發FBRetainCycleDetector的項目)中提出的想法。

咱們可使用的是ABI(application binary interface for blocks - 應用程序二進制Block接口)。它描述了Block在內存中的樣子。若是咱們知道咱們在處理的引用是一個Block,咱們能夠把它丟在一個假的結構體中來模仿Block。在放到一個C語言的結構體以後,咱們能夠知道Block所持有的對象。不幸的是,咱們不知道這些引用是強引用仍是弱引用。

爲了解決這個問題,咱們使用了一個黑盒技術。咱們建立一個對象來假扮咱們想要調查的Block。由於咱們知道Block的接口,咱們知道在哪能夠找到Block持有的引用。咱們僞造的對象將會擁有「釋放檢測(release detectors)」來代替這些引用。釋放檢測器是一些很小的對象,它們會觀察發送給它們的釋放消息。當持有者想要放棄它的持有的時候,這些消息會發送給強引用對象。當咱們釋放咱們僞造的對象的時候,咱們能夠檢測哪些檢測器接收到了這些消息。只要知道哪些索引在僞造的對象的檢測器中,咱們就能夠找到原來Block中實際持有的對象。

自動化

讓這工具真正閃光的是,在工程師內部構建的時候,它會連續的、自動的運行。

客戶端部分自動化是簡單的。咱們在定時器上運行循環引用檢測器,按期掃描內存去尋找循環引用,雖然這不是徹底沒有問題。當咱們第一次運行分析器的時候,咱們意識到它不足以很快的掃描整個內存空間。當它開始檢測的時候,咱們須要給它提供一組候選對象。

爲了更有效的解決這個問題,咱們開發了FBAllocationTracker。這個工具會主動跟蹤NSObject子類的建立和釋放。它能夠以一個很小的性能開銷來獲取任何類的任何實例。

對於客戶端的自動化,只要在NSTimer上使用FBRetainCycleDetector,再用FBAllocationTracker來抓取實例來配合跟蹤就行。

如今,讓咱們來仔細看看後臺會發生什麼。

循環引用能夠包含任何數量的對象。一個壞的鏈接會致使不少環的時候,這就複雜了。

在環中,A→B是一個壞鏈接,建立了兩個環:A-B-C-D 和 A-B-C-E。

這有兩個問題:

  1. 咱們不想給一個壞鏈接致使的兩個循環引用分別標記。

  2. 咱們不想給可能表明兩個問題的兩個循環引用一塊兒標記,即便它們共享一個鏈接。

因此咱們須要給循環引用定義簇組(clusters),鑑於這些啓發,咱們寫了個算法來找到這些問題。

  1. 在給定的時間收集全部的環。

  2. 對於每個環,提取Facebook特定的類名。

  3. 對於每個環,找到包含在環內的被報告的最小的環。

  4. 依據上面的最小環,將環添加到組中。

  5. 只報告最小環。

最後一部分是找出誰第一時間偶然引入了循環引用。咱們能夠經過環中的」git/hg責任」的部分代碼來猜想最近的變化所致使的問題。最後一個接觸這個代碼的人將會收到修復代碼的任務。

整個系統以下:

手動性能分析

雖然自動化有助於簡化發現循環引用的過程,下降人員的消耗,手動性能分析依然有它的用武之地。咱們建立的另外一個工具容許任何人查看內存使用,甚至不須要把他的手機插到電腦上。

FBMemoryProfiler能夠很容易的添加到任何應用程序,可讓你手動配置構建文件,可讓你在應用程序內運行循環應用檢測。它會借用FBAllocationTrackerFBRetainCycleDetector來實現此功能。

segmentfault不支持嵌入視頻,建議跳轉到博客去瀏覽文章。
http://7i7i81.com1.z0.glb.clouddn.com/blogvideo_memoryleak_2.mp4

生成(Generations)

FBMemoryProfiler的一個很偉大的特性是「生成追蹤(generation tracking)」,相似於蘋果的Instruments的生成追蹤。生成只是簡單的在兩次標記之間拍攝全部仍然活着的對象的快照。

使用FBMemoryProfiler的界面,咱們能夠標記生成,例如,分配三個對象。而後咱們標記另外一個生成,以後繼續分配對象。第一個生成包含咱們一開始的三個對象。若是任意一個對象被釋放了,它會從咱們第二個生成中移除。

當咱們有一個重複的任務,咱們認爲可能會內存泄露的時候,生成追蹤是頗有用的,例如,導航View Controller的進出。在每次開始咱們的任務的時候,咱們標記一個生成,而後,對以後的每一個生成進行調查。若是一個對象不該該活這麼長時間,咱們能夠在FBMemoryProfiler界面清楚地看到。

Check Out

不管你的應用程序是大是小,功能是可能是少,好的工程師都應有好的內存管理。在這些工具的幫助之下,咱們能夠更簡單的找到並修復這些內存泄露,因此咱們能夠花費更少的時間去手動處理,這樣就能夠有更多的時間去編寫更好的代碼。咱們也但願你能夠發現它們是有用的。在Github上check out下來吧。FBRetainCycleDetector, FBAllocationTrackerFBMemoryProfiler

備註

使用方式能夠參考FBMemoryProfiler上的 Usage,或者也能夠參考個人另一篇博客:FBMemoryProfiler 基礎教程

相關文章
相關標籤/搜索