看到facebook的一套內存泄漏檢測工具,感受不錯,想要查看原文能夠點擊(http://t.cn/Rqi6Saz),後續在去分析相關的開源工具git
手機設備上的內存屬於共享資源。應用不合理的使用它會致使內存耗盡,崩潰以及致使性能的大幅度下降。算法
Facebook的iOS客戶端有許多特性,它們共享同一個內存空間,因此假如某個特定的特性消耗太多的內存,這會影響到整個應用,好比某個特性意外的出現內存泄漏。app
當咱們爲一組對象分配內存,若是使用完沒有釋放相應的內存就會致使內存泄漏狀況的發生,這意味着系統沒法回收該內存來用於其它用途,最終致使內存耗盡。工具
在Facebook,許多工程師在不一樣的代碼倉庫上工做,這不可避免會有內存泄漏的狀況發生,當出現這種狀況時,咱們須要快速的找到並修復它們。佈局
已經有一些工具來輔助咱們找到內存泄漏,不過須要大量的人工干預:性能
打開Xcode,選擇build for profiling.學習
載入Instruments工具ui
使用app, 嘗試儘量多的重現場景和行爲編碼
查看instrument的leaks/memoryspa
查找內存泄漏的根源
修復問題
這意味着每次都須要大量的手動操做,致使咱們可能在開發週期內沒法儘早的定位以及修復內存泄漏的問題。
若是該過程可以自動化,咱們就可以在太多開發者干預的狀況下快速找到內存泄漏。爲此咱們構建一系列的工具來自動化查找以及修復代碼倉庫中的一些問題,這些工具包括:FBRetainCycleDetector, FBAllocationTracker以及FBMemoryProfiler
Retain cycles(循環引用)
Objective-C使用引用計數來管理內存以及釋放不使用的對象,任何一個對象能夠持有(retain)其它對象,這樣只要前面的對象須要使用它,該對象就會一直保存在內存,能夠認爲對象「擁有」其它對象。
大部分狀況下這都工做的很好,可是假如兩個對象最後互相「擁有」對方,直接或着更多經過其它對象間接的鏈接它們,這就會陷入一個僵局。這種持有引用的環就叫作循環引用。
循環引用會致使一系列的問題,最優的狀況是對象一直在RAM中只佔據一點點的空間。若是泄漏的對象只是作一些可有可無的工做,那結果只是應用會少一些可以使用的內存。最差的狀況下,假如泄漏的內存超過了可以使用的內存空間,應用可能會崩潰。
在手動性能分析的過程當中,發現咱們每每會有不少循環引用的狀況,在開發的時候很容易出現循環引用,但卻不容易在後面找到。Retain Cycle Detector能夠幫助咱們很容易的找到它們。
運行時檢測循環引用
在Objective-C中查找循環引用相似於在一個有向無環圖(directed acyclic graph)中查找環,節點就是對象,而邊則是對象之間的引用(若是對象A retain 對象B,那麼A到B之間就存在引用)。咱們的Objective-C對象已經存在於咱們的圖當中,咱們要作的就是使用深度優先方法歷遍搜索它。(看到算法的重要性了。。)
雖然這只是個簡單的抽象,但實際效果卻不錯。咱們必須確保咱們可以像節點同樣使用對象,對於每一個對象,咱們可以獲取它所引用的全部對象,這些引用多是weak或者strong,不過只有strong的引用纔會致使循環引用。所以對於每一個對象,咱們須要知道如何找出這些強引用。幸運的是,Objective-C提供了一套強有力、內省的運行庫,可以提供咱們足夠的數據去挖掘這張圖。
圖中的節點能夠是一個對象或一個block,讓咱們分別討論。
對象(Objects)
運行時有許多工具可以讓咱們對對象進行內省學習,咱們要作的第一件事就是獲取對象全部實例變量的佈局(ivar layout)
//runtime.h
const char *class_getIvarLayout(Class cls);
const char *class_getWeakIvarLayout(Class cls);
對於一個給定的對象,實例變量佈局描述了咱們該去哪查找其所引用的其它對象。它會提供一個「索引」,這個索引表明着偏移量(offset),咱們在對象地址上加上該偏移量來獲取它所引用對象的地址。運行時還容許咱們獲取「弱引用實例變量的佈局(weak ivar layout)」,咱們能夠認爲這兩種佈局的差異在於強引用佈局。
這也部分支持Objective-C++。在Objective-C++中,咱們能夠在結構體中定義對象,但這不會在實例變量佈局(ivar layout)中獲取到,運行時提供「類型編碼(type encoding)」來解決這個問題。對於每一個實例變量,類型編碼描述了變量如何結構化的。若是變量是一個結構體,它描述了變量包含的字段和類型。咱們經過解析類型編碼來找出哪些實例變量是Objective-C對象。咱們計算出它們的偏移量(offset),而後在佈局中找到它們指向對象的地址。
有些邊緣狀況咱們不會深刻。大部分是一些不一樣的集合,咱們須要歷遍它們來獲取它們持有的對象,這可能會有一些反作用。
Blocks
Blocks跟對象有些區別。運行時沒有讓咱們很容易看到它們的佈局,但咱們仍然能夠猜想。在處理Blocks的時候,咱們採用Mike Ash在他的Circle項目中的思路。
咱們可使用的是ABI(application binary interface for blocks),它描述了block在內存中的樣子。若是咱們知道在處理的引用是一個block,那咱們可使用一個假的結構體來模擬該block對象。將block轉換成一個C結構體後,咱們就能夠知道block持有哪些對象,不過不幸的是,咱們不知道這些引用是強引用仍是弱引用。
爲了解決這個問題,咱們使用一個黑盒技術,咱們建立一個僞造對象僞裝是咱們要研究的block。由於咱們知道block的接口,咱們知道在哪能夠找到block持有的引用,僞造的對象使用」release detectors」來替代這些引用。release detectors是一些小的對象,它們會觀察發送給他們release的消息。當持有者想要放棄對象的擁有權時,release消息就會發送給它所強引用的對象。當咱們釋放該僞造的對象後,能夠檢查哪些detectors收到了release消息。知道了接收release消息的detectors的索引位置以後,咱們就能夠找到block對象所持有的強引用對象。
自動化
這些工具在工程師內部構建的時候可以持續自動的運行,確實閃閃發光。
在客戶端的自動化很簡單。咱們定時的運行Retain Cycle Detector,按期的去掃描內存查找循環引用,不過這並非這麼一路順風。咱們第一次運行Detector時,咱們意識到它沒法很快的掃描整個內存空間,咱們須要首先提供一組候選對象來讓Detector檢測。
爲了更有效的處理上述問題,咱們建立了FBAllocationTracker。這個工具可以記錄全部NSObject子類對象的建立和銷燬,它可以以極小的性能代價在任意時刻快速獲取任何類的對象實例。
客戶端有了上述的自動化過程,意味着咱們只須要在NSTimer上運行FBRetainCycleDetector,在配合FBAllocationTracker來抓取咱們想要分析的實例便可。
如今讓咱們深刻的看一下背後具體發生了什麼。
循環引用能夠包含任意數量的對象,當因爲一個壞的鏈接(bad link)致使不少環的產生,事情就變的更復雜了。
在上面的環中,A->B就是一個壞的鏈接(bad link),它建立了兩個環:A-B-C-D和A-B-C-E。這會有兩個問題:
若是由同一個壞的鏈接致使兩個循環引用,咱們不想用不一樣的標記來分別標記它們;
咱們不想給可能表明兩個不一樣問題的兩個循環引用一塊兒標記,即使他們共享一條鏈接。
因此咱們須要給循環引用定義類簇(clusters)。咱們寫了一個算法來找出這些問題,算法以下
1. 在給定的時間,收集全部的環;
2. 對於每一個環,提取Facebook特定的類名稱;
3. 對於每一個環,找出該環包含的最小環;
4. 將每一個環添加到由上面找到的最小環所表明的組中;
5. 只報告最小環;
最後要作的就是找到誰第一時間偶然地引入了循環引用,能夠經過對環所涉及的代碼進行’git/hg blame’,咱們猜想可能最新的代碼致使了該問題,因此最後一個接觸該代碼的人會收到一個task來修復該問題。
整個過程以下圖所示:
手動性能分析
雖然自動化能簡化發現循環引用的過程,減小開發人員的消耗,但手動性能分析仍是必不可少。咱們建立了另一個工具,容許任何人查看內存的使用狀況,甚至不須要把手機插到電腦上。
FBMemoryProfiler能夠很容易的添加到任意應用,讓你在應用內部手動配置構建文件以及運行循環引用檢測,該工具藉助FBAllocationTracker和FBRetainCycleDetector實現該功能。
代(Generations)
FBMemoryProfiler一個最大的特性是提供」代追蹤(generation tracking)」,相似蘋果instruments的generation tracking,Generations是兩個時間標記之間全部仍然活着的對象的快照。
使用FBMemoryProfiler的界面,咱們能夠標記一個generation,好比建立了三個對象;而後咱們標記另外一個generation並繼續建立對象。第一個generation包含了咱們三個最初的對象,若是有對象被釋放了,那麼該對象就會在第二個generation中被移除。
假若有一個重複的任務,咱們認爲可能有內存泄漏的狀況發生,這時候Generation tracking就頗有效了。好比導航進入一個View Controller而後退出,每次開始任務以前,咱們標記一個generation,而後在每次generation標記之間進行調查,若有對象並不該該存活那麼長,那咱們能夠在FBMemoryProfiler的界面上清楚的看到。