內存泄露監測

iOS 內存泄露監測
144  做者 謝謝生活 已關注
2017.05.19 17:38* 字數 4235 閱讀 209評論 0喜歡 6
iOS可能存在的內存泄露:

block 循環引用。當一個對象有一個block屬性,而block屬性又引用這個對象自己那麼要形成循環引用。這個時候就用___weak聲明下對象,用對象的弱引用指針。
頭文件相互包含。那麼先在.h文件用前向引用聲明,@class(類名);而後在.m文件導入#import " AHMessageCell"(類頭文件)
移除通知 [[NSNotificationCenter defaultCenter]removeObserver:self];、
移除NSTimer
  [_timer invalidate];
    _timer = nil;
移除觀察者
//添加觀察者
    [self addObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#> options:<#(NSKeyValueObservingOptions)#> context:<#(nullable void *)#>]
//移除觀察者
    [self removeObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#>];
timer,觀察者,通知的移除。通常的開發者都是放到dealloc中,可是這樣不能保證必定可以移除成功。能夠更加實際狀況移除,能夠在viewWillAppear中添加,viewWillDisappear中移除,也能夠強制移除。

iOS內存泄露測試:能夠用xcode自帶instrument工具,如:leaks、Analyze、allocation,也能夠用第三方工具。

一: leaks

打開Xcode7自帶的Instruments



打開Instruments
按上面操做,build成功後跳出Instruments工具,選擇Leaks選項

選擇以後界面以下圖:


打開leaks
到這裏以後,咱們前期的準備工做作完啦,下面開始正式的測試!

1.選中Xcode先把程序(command + R)運行起來

2.再選中Xcode,按快捷鍵(command + control + i)運行起來,此時Leaks已經跑起來了

3.因爲Leaks是動態監測,因此咱們須要手動操做APP,一邊操做,一邊觀察Leaks的變化,當出現紅色叉時,就監測到了內存泄露,點擊右上角的第二個,進行暫停檢測(也可繼續檢測,當多個時暫停,一次處理了多個).如圖所示:


4.下面就是定位修改了,此時選中有紅色柱子的Leaks,下面有個"田"字方格,點開,選中Call Tree顯示以下圖界面

找到內存泄露位置
5.下面就是最關鍵的一步,在這個界面的右下角有若干選框,選中Invert Call Tree 和Hide System Libraries,(紅圈範圍內)顯示以下:

監測回調函數
到這裏就算基本完成啦,這裏顯示的就是內存泄露代碼部分,那麼如今還差一步:定位!

6.選中顯示的若干條中的一條,雙擊,會自動跳到內存泄露代碼處,如圖所示



查看回調函數
7.找到了內存泄露的地方,那麼咱們就能夠修改便可。

二:Analyze—靜態分析

顧名思義,靜態分析不須要運行程序,就能檢查到存在內存泄露的地方。

使用方法:打開Xcode,command + shift + B;或者Xcode - Product - Analyze;
常見的三種泄露情形:
(1)建立了一個對象,可是並無使用。Xcode提示信息: Value Stored to 'number' is never read 。翻譯一下:存儲在'number'裏的值從未被讀取過。
(2)建立了一個(指針可變的)對象,且初始化了,可是初始化的值一直沒讀取過。Xcode提示信息: Value Stored to 'str' during its initialization is never read
(3)調用了讓某個對象引用計數加1的函數,但沒有調用相應讓其引用計數減1的函數。Xcode提示信息: Potential leak of an object stored into 'subImageRef' 。 翻譯一下:subImageRef對象的內存單元有潛在的泄露風險。
貼上Demo代碼:
/**
 * 情 形 一:建立了一個對象,可是並無使用。
 * 提示信息:Value Stored to 'number' is never read
 * 翻譯一下:存儲在'number'裏的值從未被讀取過,
 */
- (void)leakOne {
    NSString *str1 = [NSString string];
    NSNumber *number;
    number = @(str1.length);
    /*
     說咱們沒有讀取過它,那就讀取一下,好比打開下面這句代碼,對它發送class消息,就再也不會有這個提示了。
     固然最好的方法仍是將有關number的代碼都刪掉,由於,你只對number賦值,又不使用,那幹嗎建立出來呢。
     這是一個比較常見和典型的錯誤,也很容易檢查出來
     */
    // [number class];
}

/**
 * 情 形 二:建立了一個(指針可變的)對象,且初始化了,可是初始化的值一直沒讀取過。
 * 提示信息:Value Stored to 'str' during its initialization is never read
 */
- (void)leakTwo {
    NSString *str = [NSString string]; // 建立並初始化str,此時已經有一個內存單元保存str初始化的值
    // NSString *str; // 這樣就內存不泄露,由於str是可變的,只須要先聲明就行。
    // printf("str前 = %p\n",str);
    str = @"ceshi";             // str被改變了,指向了"ceshi"所在的地址,指針改變了,但以前保存初始化值的內存空間還未釋放,保存str初始化值的內存單元泄露了。
    // printf("str後 = %p\n",str); // 指針改變了
    [str class];

    // 再舉兩個例子,同理

    NSArray *arr = [NSArray array];
    // printf("arr前 = %p\n",arr);
    // NSArray *arr;            // 這樣就內存不泄露
    arr = @[@"1",@"2"];
    // printf("arr後 = %p\n",arr); // 指針改變了
    [arr class];

    CGRect rect = self.view.frame;
    // CGRect rect = CGRectZero; // 這樣就內存不泄露
    rect = CGRectMake(0, 0, 0, 0);
    NSLog(@"rect = %@",NSStringFromCGRect(rect));
}

/**
 * 情 形 三:調用了讓某個對象引用計數加1的函數,但沒有調用相應讓其引用計數減1的函數。
 * 提示信息:Potential leak of an object stored into 'subImageRef'
 * 翻譯一下:subImageRef對象的內存單元有潛在的泄露風險
 */
- (void)leakThree {
    CGRect rect = CGRectMake(0, 0, 50, 50);
    UIImage *image;
    CGImageRef subImageRef = CGImageCreateWithImageInRect(image.CGImage, rect); // subImageRef 引用計數 + 1;

    UIImage* smallImage = [UIImage imageWithCGImage:subImageRef];

    // 應該調用對應的函數,讓subImageRef的引用計數減1,就不會泄露了
    // CGImageRelease(subImageRef);

    [smallImage class];
    UIGraphicsEndImageContext();
}
監測結果:


可能存在內存泄露的地方
三:allocation使用

這個時候咱們經過Allocation能夠進行內存分析,將Xcode切換爲Release狀態,經過Product→Profile(Cmd+i)找到Allocations:



代開allocation
1.紅色的按鈕是表示中止和啓動應用程序,不要理解成了暫停,Objective-C全部的對象都是在堆上分配的,記得勾選一下All Heap Allocations:


開始監測
2.點擊All Heap Allocation,勾選Call Tree,同時不查看系統的函數庫:



監測回調函數
3.具體方法佔用的內存,能夠逐級點開,效果以下:



內存佔用
以上是常規的Allocations使用,關於第二張圖的有框中的幾個選項能夠解釋一下:
Separate by Thread: 每一個線程應該分開考慮,考慮到應用程序中GCD的存在;
Invert Call Tree: 從上倒下跟蹤堆棧,這意味着你看到的表中的方法,將已從第0幀開始取樣,利用棧的先進後出的特性,咱們能夠在棧頂看到最近調用的函數;
Hide System Libraries: 勾選此項會顯示app的代碼,這是很是有用的;
Flatten Recursion: 遞歸函數, 每一個堆棧跟蹤一個條目;

左側有幾個比較有用的選項:
All Objects Created
Created & Still Living
Created & Destroyed



內存監測
4.Allocation 分析技巧
經過以上方法能夠對應用的總體內存使用狀況有所瞭解,但內存不合理使用致使的內存警告每每是部分代碼或視圖致使的,咱們每每要關注於某段時間或操做過程當中內存的分配和使用狀況,Allocation提供了這種功能。
好比在進入一個視圖前或操做前,咱們在Allocation面板左側點擊Mark Generation,這時候會產生Generation A節點,顯示內存當前的狀況:



比較測出內存泄露點
咱們能夠在進入視圖後再點一次Mark Generation,在視圖退出後再點一次Mark,這樣三次產生的 Generation分別記錄了進入前、進入後、關閉後,再最後一個Generation應該內存被合理釋放,不然就表明了在這個視圖或操做中有泄漏或不合理的地方。
以上只是Allocation的基本運用,設計出一套使用Allocation來合理測試的方案是比較複雜的,後續慢慢介紹。

四:MLeaksFinder

MLeaksFinder 提供了內存泄露檢測更好的解決方案。只須要引入 MLeaksFinder,就能夠自動在 App 運行過程檢測到內存泄露的對象並當即提醒,無需打開額外的工具,也無需爲了檢測內存泄露而一個個場景去重複地操做。MLeaksFinder 目前能自動檢測 UIViewController 和 UIView 對象的內存泄露,並且也能夠擴展以檢測其它類型的對象。
MLeaksFinder 的使用很簡單,參照 https://github.com/Zepo/MLeaksFinder,基本上就是把 MLeaksFinder 目錄下的文件添加到你的項目中,就能夠在運行時(debug 模式下)幫助你檢測項目裏的內存泄露了,無需修改任何業務邏輯代碼,並且只在 debug 下開啓,徹底不影響你的 release 包。
當發生內存泄露時,MLeaksFinder 會中斷言,並準確的告訴你哪一個對象泄露了。這裏設計爲中斷言而不是打日誌讓程序繼續跑,是由於不少人不會去看日誌,斷言則能強制開發者注意到並去修改,而不是犯拖延症。
中斷言時,控制檯會有以下提示,View-ViewController stack 從上往下看,該 stack 告訴你,MyTableViewController 的 UITableView 的 subview UITableViewWrapperView 的 subview MyTableViewCell 沒被釋放。並且,這裏咱們能夠確定的是 MyTableViewController,UITableView,UITableViewWrapperView 這三個已經成功釋放了。
* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Possibly Memory Leak.In case that MyTableViewCell should not be dealloced, override -willDealloc in MyTableViewCell by returning NO.View-ViewController stack: ( MyTableViewController, UITableView, UITableViewWrapperView, MyTableViewCell)'

從 MLeaksFinder 的使用方法能夠看出,MLeaksFinder 具有如下優勢:
使用簡單,不侵入業務邏輯代碼,不用打開 Instrument
不須要額外的操做,你只需開發你的業務邏輯,在你運行調試時就能幫你檢測
內存泄露發現及時,更改完代碼後一運行即能發現(這點很重要,你立刻就能意識到哪裏寫錯了)
精準,能準確地告訴你哪一個對象沒被釋放

原理(http://wereadteam.github.io/2016/02/22/MLeaksFinder/?from=singlemessage&isappinstalled=0#u539F_u7406)
MLeaksFinder 一開始從 UIViewController 入手。咱們知道,當一個 UIViewController 被 pop 或 dismiss 後,該 UIViewController 包括它的 view,view 的 subviews 等等將很快被釋放(除非你把它設計成單例,或者持有它的強引用,但通常不多這樣作)。因而,咱們只需在一個 ViewController 被 pop 或 dismiss 一小段時間後,看看該 UIViewController,它的 view,view 的 subviews 等等是否還存在。
具體的方法是,爲基類 NSObject 添加一個方法 -willDealloc
方法,該方法的做用是,先用一個弱指針指向 self,並在一小段時間(3秒)後,經過這個弱指針調用 -assertNotDealloc,而 -assertNotDealloc 主要做用是直接中斷言。

- (BOOL)willDealloc { __weak id weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakSelf assertNotDealloc]; }); return YES;}- (void)assertNotDealloc { NSAssert(NO, @「」);}
這樣,當咱們認爲某個對象應該要被釋放了,在釋放前調用這個方法,若是3秒後它被釋放成功,weakSelf 就指向 nil,不會調用到 -assertNotDealloc
方法,也就不會中斷言,若是它沒被釋放(泄露了),-assertNotDealloc
就會被調用中斷言。這樣,當一個 UIViewController 被 pop 或 dismiss 時(咱們認爲它應該要被釋放了),咱們遍歷該 UIViewController 上的全部 view,依次調 -willDealloc,若3秒後沒被釋放,就會中斷言。

在這裏,有幾個問題須要解決:
不入侵開發代碼
這裏使用了 AOP 技術,hook 掉 UIViewController 和 UINavigationController 的 pop 跟 dismiss 方法,關於如何 hook,請參考 Method Swizzling。

遍歷相關對象
在實際項目中,咱們發現有時候一個 UIViewController 被釋放了,但它的 view 沒被釋放,或者一個 UIView 被釋放了,但它的某個 subview 沒被釋放。這種內存泄露的狀況很常見,所以,咱們有必要遍歷基於 UIViewController 的整棵 View-ViewController 樹。咱們經過 UIViewController 的 presentedViewController 和 view 屬性,UIView 的 subviews 屬性等遞歸遍歷。對於某些 ViewController,如 UINavigationController,UISplitViewController 等,咱們還須要遍歷 viewControllers 屬性。

構建堆棧信息
須要構建 View-ViewController stack 信息以告訴開發者是哪一個對象沒被釋放。在遞歸遍歷 View-ViewController 樹時,子節點的 stack 信息由父節點的 stack 信息加上子結點信息便可。

例外機制
對於有些 ViewController,在被 pop 或 dismiss 後,不會被釋放(好比單例),所以須要提供機制讓開發者指定哪一個對象不會被釋放,這裏能夠經過重載上面的 -willDealloc
方法,直接 return NO 便可。

特殊狀況
對於某些特殊狀況,釋放的時機不大同樣(好比系統手勢返回時,在劃到一半時 hold 住,雖然已被 pop,但這時還不會被釋放,ViewController 要等到徹底 disappear 後才釋放),須要作特殊處理,具體的特殊處理視具體狀況而定。

系統View
某些系統的私有 View,不會被釋放(多是系統 bug 或者是系統出於某些緣由故意這樣作的,這裏就不去深究了),所以須要創建白名單

手動擴展
MLeaksFinder目前只檢測 ViewController 跟 View 對象。爲此,MLeaksFinder 提供了一個手動擴展的機制,你能夠從 UIViewController 跟 UIView 出發,去檢測其它類型的對象的內存泄露。以下所示,咱們能夠檢測 UIViewController 底下的 View、Model:

- (BOOL)willDealloc { if (![super willDealloc]) { return NO; } MLCheck(self.viewModel); return YES;}
這裏的原理跟上面的是同樣的,宏 MLCheck() 作的事就是爲傳進來的對象創建 View-ViewController stack 信息,並對傳進來的對象調用 -willDealloc
方法。

五:faceBook提供的內存泄露自動化測試:

FBRetainCycleDetector、FBAllocationTracker、FBMemoryProfiler。

讓這工具真正閃光的是,在工程師內部構建的時候,它會連續的、自動的運行。
客戶端部分自動化是簡單的。咱們在定時器上運行循環引用檢測器,按期掃描內存去尋找循環引用,雖然這不是徹底沒有問題。當咱們第一次運行分析器的時候,咱們意識到它不足以很快的掃描整個內存空間。當它開始檢測的時候,咱們須要給它提供一組候選對象。
爲了更有效的解決這個問題,咱們開發了FBAllocationTracker。這個工具會主動跟蹤NSObject
子類的建立和釋放。它能夠以一個很小的性能開銷來獲取任何類的任何實例。
對於客戶端的自動化,只要在NSTimer
上使用FBRetainCycleDetector,再用FBAllocationTracker來抓取實例來配合跟蹤就行。
如今,讓咱們來仔細看看後臺會發生什麼。
循環引用能夠包含任何數量的對象。一個壞的鏈接會致使不少環的時候,這就複雜了。



在環中,A→B是一個壞鏈接,建立了兩個環:A-B-C-D 和 A-B-C-E。
這有兩個問題:
咱們不想給一個壞鏈接致使的兩個循環引用分別標記。
咱們不想給可能表明兩個問題的兩個循環引用一塊兒標記,即便它們共享一個鏈接。

因此咱們須要給循環引用定義簇組(clusters),鑑於這些啓發,咱們寫了個算法來找到這些問題。
在給定的時間收集全部的環。
對於每個環,提取Facebook特定的類名。
對於每個環,找到包含在環內的被報告的最小的環。
依據上面的最小環,將環添加到組中。
只報告最小環。

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



手動性能分析
雖然自動化有助於簡化發現循環引用的過程,下降人員的消耗,手動性能分析依然有它的用武之地。咱們建立的另外一個工具容許任何人查看內存使用,甚至不須要把他的手機插到電腦上。
FBMemoryProfiler能夠很容易的添加到任何應用程序,可讓你手動配置構建文件,可讓你在應用程序內運行循環應用檢測。它會借用FBAllocationTracker和FBRetainCycleDetector來實現此功能。

生成(Generations)
FBMemoryProfiler的一個很偉大的特性是「生成追蹤(generation tracking)」,相似於蘋果的Instruments的生成追蹤。生成只是簡單的在兩次標記之間拍攝全部仍然活着的對象的快照。
使用FBMemoryProfiler的界面,咱們能夠標記生成,例如,分配三個對象。而後咱們標記另外一個生成,以後繼續分配對象。第一個生成包含咱們一開始的三個對象。若是任意一個對象被釋放了,它會從咱們第二個生成中移除。



當咱們有一個重複的任務,咱們認爲可能會內存泄露的時候,生成追蹤是頗有用的,例如,導航View Controller的進出。在每次開始咱們的任務的時候,咱們標記一個生成,而後,對以後的每一個生成進行調查。若是一個對象不該該活這麼長時間,咱們能夠在FBMemoryProfiler界面清楚地看到。
Check Out
不管你的應用程序是大是小,功能是可能是少,好的工程師都應有好的內存管理。在這些工具的幫助之下,咱們能夠更簡單的找到並修復這些內存泄露,因此咱們能夠花費更少的時間去手動處理,這樣就能夠有更多的時間去編寫更好的代碼。咱們也但願你能夠發現它們是有用的。在Github上check out下來吧。FBRetainCycleDetector, FBAllocationTracker 和 FBMemoryProfiler。
相關文章
相關標籤/搜索