在iOS上自動檢測記憶體洩露

Forkong發表於2016-04-16

手機裝置的記憶體是一個共享資源。應用程式可能會不當的耗盡記憶體、崩潰,或者遭遇大幅度的效能降低。

Facebook iOS客戶端有很多功能,並且它們共享同一塊記憶體空間。如果任何特定的功能消耗過多的記憶體,就會影響到整個應用程式。這是可能發生的,比如,這個功能導致了記憶體洩露。

當我們分配了一塊記憶體,並設定了物件之後,如果在使用完了之後忘記釋放,這就會發生記憶體洩露。這意味著系統是無法回收記憶體並交予他人使用,這也最終意味著我們的記憶體將會逐漸耗盡。

在Facebook,我們有很多工程師在程式碼庫的不同部分上工作。這不可避免的會發生記憶體洩露。當發生記憶體洩露之後,我們需要儘快找到並修復它們。

一些工具已經可以找到記憶體洩露,但是它們需要大量的人工干預:

  1. 開啟Xcode,給效能分析(profiling)編譯。
  2. 載入Instruments。
  3. 使用應用程式,嘗試儘可能多的重現場景和行為。
  4. 檢視記憶體和洩露。
  5. 追蹤記憶體洩露的根源。
  6. 修復這個問題。

這意味著每次都需要重複大量的手動操作。為此,在我們的開發週期上,我們可能無法儘可能早的定位和修復記憶體洩露問題。

自動化可以在不需要更多開發者的情況下,更快的找到記憶體洩露。為了解決這個問題,我們做了一套工具來自動化的處理和修復我們程式碼庫中的一些問題。今天,我們很興奮的釋出這些工具:FBRetainCycleDetectorFBAllocationTrackerFBMemoryProfiler

迴圈引用(Retain cycles)

Objective-C 使用引用計數去管理記憶體和釋放不使用的物件。記憶體中的任何一個物件都可以持有(retain)其他的物件,只要前面的物件需要它,物件就會一直保持在記憶體中。檢視這個的一個方法是這個物件持有其他的物件。

在大部分時間內,這都工作的很好,但當兩個物件互相持有的時候,這就會陷入一個僵局。直接,或者更常見的,通過間接物件連線它們。這種持有引用的環我們叫做迴圈引用(Retain cycles)。

迴圈引用會導致一些列的問題。最好的情況下,物件只會在記憶體中佔有一點點位置。如果這個被洩露的物件正積極地做個一些不平凡的事情,應用程式的其他部分就只會有更少的記憶體。最壞的情況下,如果洩露導致使用超出可用記憶體的容量,那麼,應用程式會崩潰。

在手動效能分析期間,我們發現,我們往往有一些迴圈引用。我們很容易引起記憶體洩露,但是很難找到它們。迴圈引用檢測器可以很容易的找到它們。

在執行時檢測迴圈引用

在 Objective-C 中找迴圈引用類似於在一個有向無環圖(directed acyclic graph)中找環, 而節點就是物件,邊就是物件之間的引用(如果物件A持有物件B,那麼,A到B之間就存在著引用)。我們的 Objective-C 物件已經在我們的圖中,我們要做的就是用深度優先搜尋遍歷它。

這有點抽象,但效果很好。我們必須確保我們可以像節點一樣使用物件,對於每個物件,我們都可以獲取到它引用的所有物件。這些引用可能是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來實現此功能。

生成(Generations)

FBMemoryProfiler的一個很偉大的特性是“生成追蹤(generation tracking)”,類似於蘋果的Instruments的生成追蹤。生成只是簡單的在兩次標記之間拍攝所有仍然活著的物件的快照。

使用FBMemoryProfiler的介面,我們可以標記生成,例如,分配三個物件。然後我們標記另一個生成,之後繼續分配物件。第一個生成包含我們一開始的三個物件。如果任意一個物件被釋放了,它會從我們第二個生成中移除。

當我們有一個重複的任務,我們認為可能會記憶體洩露的時候,生成追蹤是很有用的,例如,導航View Controller的進出。在每次開始我們的任務的時候,我們標記一個生成,然後,對之後的每個生成進行調查。如果一個物件不應該活這麼長時間,我們可以在FBMemoryProfiler介面清楚地看到。

Check Out

無論你的應用程式是大是小,功能是多是少,好的工程師都應有好的記憶體管理。在這些工具的幫助之下,我們可以更簡單的找到並修復這些記憶體洩露,所以我們可以花費更少的時間去手動處理,這樣就可以有更多的時間去編寫更好的程式碼。我們也希望你可以發現它們是有用的。在Github上check out下來吧。FBRetainCycleDetectorFBAllocationTracker 和 FBMemoryProfiler

相關文章