自動監測內存泄漏(譯)

XavierCrack 9年前發布 | 5K 次閱讀 iOS開發

看到非死book的一套內存泄漏檢測工具,感覺不錯,想要查看原文可以點擊 這里 ,后續在去分析相關的開源工具

手機設備上的內存屬于共享資源。應用不合理的使用它會導致內存耗盡,崩潰以及導致性能的大幅度降低。

非死book的iOS客戶端有許多特性,它們共享同一個內存空間,所以假如某個特定的特性消耗太多的內存,這會影響到整個應用,比如某個特性意外的出現內存泄漏。

當我們為一組對象分配內存,如果使用完沒有釋放相應的內存就會導致內存泄漏情況的發生,這意味著系統無法回收該內存來用于其它用途,最終導致內存耗盡。

在非死book,許多工程師在不同的代碼倉庫上工作,這不可避免會有內存泄漏的情況發生,當出現這種情況時,我們需要快速的找到并修復它們。

已經有一些工具來輔助我們找到內存泄漏,不過需要大量的人工干預:

  1. 打開Xcode,選擇build for profiling.
  2. 載入Instruments工具
  3. 使用app, 嘗試盡可能多的重現場景和行為
  4. 查看instrument的leaks/memory
  5. 查找內存泄漏的根源
  6. 修復問題

這意味著每次都需要大量的手動操作,導致我們可能在開發周期內無法盡早的定位以及修復內存泄漏的問題。

如果該過程能夠自動化,我們就能夠在太多開發者干預的情況下快速找到內存泄漏。為此我們構建一系列的工具來自動化查找以及修復代碼倉庫中的一些問題,這些工具包括: FBRetainCycleDetector , FBAllocationTracker 以及 FBMemoryProfiler

Retain cycles(循環引用)

Objective-C使用引用計數來管理內存以及釋放不使用的對象,任何一個對象可以持有(retain)其它對象,這樣只要前面的對象需要使用它,該對象就會一直保存在內存,可以認為對象“擁有”其它對象。

大部分情況下這都工作的很好,但是假如兩個對象最后互相“擁有”對方,直接或著更多通過其它對象間接的連接它們,這就會陷入一個僵局。這種持有引用的環就叫做循環引用。

retain cycle

循環引用會導致一系列的問題,最優的情況是對象一直在RAM中只占據一點點的空間。如果泄漏的對象只是做一些無關緊要的工作,那結果只是應用會少一些可使用的內存。最差的情況下,假如泄漏的內存超過了可使用的內存空間,應用可能會崩潰。

在手動性能分析的過程中,發現我們往往會有很多循環引用的情況,在開發的時候很容易出現循環引用,但卻不容易在后面找到。Retain Cycle Detector可以幫助我們很容易的找到它們。

運行時檢測循環引用

在Objective-C中查找循環引用類似于在一個有向無環圖(directed acyclic graph)中查找環,節點就是對象,而邊則是對象之間的引用(如果對象A retain 對象B,那么A到B之間就存在引用)。我們的Objective-C對象已經存在于我們的圖當中,我們要做的就是使用深度優先方法歷遍搜索它。(看到算法的重要性了。。)

depth-first search

雖然這只是個簡單的抽象,但實際效果卻不錯。我們必須確保我們能夠像節點一樣使用對象,對于每個對象,我們能夠獲取它所引用的所有對象,這些引用可能是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對象所持有的強引用對象。

block

自動化

這些工具在工程師內部構建的時候能夠持續自動的運行,確實閃閃發光。

在客戶端的自動化很簡單。我們定時的運行Retain Cycle Detector,定期的去掃描內存查找循環引用,不過這并不是這么一帆風順。我們第一次運行Detector時,我們意識到它無法很快的掃描整個內存空間,我們需要首先提供一組候選對象來讓Detector檢測。

為了更有效的處理上述問題,我們創建了FBAllocationTracker。這個工具能夠記錄所有NSObject子類對象的創建和銷毀,它能夠以極小的性能代價在任意時刻快速獲取任何類的對象實例。

客戶端有了上述的自動化過程,意味著我們只需要在NSTimer上運行FBRetainCycleDetector,在配合FBAllocationTracker來抓取我們想要分析的實例即可。

現在讓我們深入的看一下背后具體發生了什么。

循環引用可以包含任意數量的對象,當由于一個壞的連接(bad link)導致很多環的產生,事情就變的更復雜了。

bad link

在上面的環中,A->B就是一個壞的連接(bad link),它創建了兩個環:A-B-C-D和A-B-C-E。這會有兩個問題:

  • 如果由同一個壞的連接導致兩個循環引用,我們不想用不同的標記來分別標記它們;
  • 我們不想給可能代表兩個不同問題的兩個循環引用一起標記,即便他們共享一條連接。

所以我們需要給循環引用定義類簇(clusters)。我們寫了一個算法來找出這些問題,算法如下

1. 在給定的時間,收集所有的環;
2. 對于每個環,提取非死book特定的類名稱;
3. 對于每個環,找出該環包含的最小環;
4. 將每個環添加到由上面找到的最小環所代表的組中;
5. 只報告最小環;

最后要做的就是找到誰第一時間偶然地引入了循環引用,可以通過對環所涉及的代碼進行’git/hg blame’,我們猜測可能最新的代碼導致了該問題,所以最后一個接觸該代碼的人會收到一個task來修復該問題。

整個過程如下圖所示:

process

手動性能分析

雖然自動化能簡化發現循環引用的過程,減少開發人員的消耗,但手動性能分析還是必不可少。我們創建了另外一個工具,允許任何人查看內存的使用情況,甚至不需要把手機插到電腦上。

FBMemoryProfiler可以很容易的添加到任意應用,讓你在應用內部手動配置構建文件以及運行循環引用檢測,該工具借助FBAllocationTracker和FBRetainCycleDetector實現該功能。

FBMemoryProfiler

代(Generations)

FBMemoryProfiler一個最大的特性是提供”代追蹤(generation tracking)”,類似蘋果instruments的generation tracking,Generations是兩個時間標記之間所有仍然活著的對象的快照。

使用FBMemoryProfiler的界面,我們可以標記一個generation,比如創建了三個對象;然后我們標記另一個generation并繼續創建對象。第一個generation包含了我們三個最初的對象,如果有對象被釋放了,那么該對象就會在第二個generation中被移除。

假如有一個重復的任務,我們認為可能有內存泄漏的情況發生,這時候Generation tracking就很有效了。比如導航進入一個View Controller然后退出,每次開始任務之前,我們標記一個generation,然后在每次generation標記之間進行調查,如有對象并不應該存活那么長,那我們可以在FBMemoryProfiler的界面上清楚的看到。

 

來自:http://ios.jobbole.com/89370/

 

 本文由用戶 XavierCrack 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!