iOS開發之效能除錯Instruments(二)
如何定位記憶體問題
今天主要講最常見的定位記憶體問題,普遍使用ARC後,開發者們從手動管理引用計數中解放出來,但開啟了ARC並不是就不會存在記憶體問題。
蘋果有句名言:ARC is only for NSObject。在iOS 中使用malloc分配的記憶體,ARC是不會處理的,需要自己進行處理。(如CGPath等)
相關概念
1.記憶體空間的劃分
一個程序佔用的記憶體空間,包括5種資料區:
(1)BSS段:通常存放未初始化的全域性變數
(2)資料段:通常存放已初始化的全域性變數
(3)程式碼段:存放程式執行程式碼
(4)堆:存放程序執行中被動態分配的記憶體段,如OC物件等
(5)棧:由編譯器自動分配釋放,存放函式引數,區域性變數等
2.記憶體溢位與記憶體洩漏的概念
記憶體溢位 out of memory,是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory
記憶體洩露 memory leak,是指程式在申請記憶體後,無法釋放已申請的記憶體空間。在iOS中一般由迴圈引用、錯用Strong/copy等原因引起。
一、Analyze-靜態分析
檢測出的常見的三種洩露
(1).建立了物件沒有使用。
(2).建立了物件,且初始化了,但初始化的值一直沒有讀取過。
Value store to ‘X’during its initialization is never.
(3).Potential leak of an object stored into 'XX'* 。 翻譯一下:XX物件的記憶體單元有潛在的洩露風險。
Analyze.png
/**
* 建立了物件,但是並沒有使用。
* Value Stored to 'XX' is never read
* 儲存在'XX'裡的值從未被讀取過,
*/
- (void)leak1 {
NSString *str = [NSString string];
NSNumber *number;
number = @(str.length);
/*
最好的方法是將有關number的程式碼都刪掉,只對number賦值不使用,那幹嘛創建出來呢。
說我們沒有讀取過它,那就讀取一下,比如開啟下面這句程式碼,對它傳送class訊息,就不再會有這個提示了。
這是一個比較常見和典型的錯誤,也很容易檢查出來
*/
// [number class];
}
/**
* 建立了一個(指標可變的)物件,且初始化了,但是初始化的值一直沒讀取過。
* Value Stored to 'str' during its initialization is never read
*/
- (void)leak2 {
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)leak3 {
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();
例子二:
CTFontRef fontRef = CTFontCreateWithName((__bridge CFStringRef)_font.fontName, _font.pointSize, NULL);
[_string addAttributes:[NSDictionary dictionaryWithObjectsAndKeys:(__bridge NSObject*)fontRef, (NSString*)kCTFontAttributeName, nil]
range:NSMakeRange(0, [_string length])];
CGColorRef colorRef = _textColor.CGColor;
[_string addAttributes:[NSDictionary dictionaryWithObjectsAndKeys:(__bridge NSObject*)colorRef,(NSString*)kCTForegroundColorAttributeName, nil]
range:NSMakeRange(0, [_string length])];
// 應該呼叫對應的函式,讓subImageRef的引用計數減1,就不會洩露了
// CFRelease(fontRef);
}
二、Allocations
Allocations是檢測程式執行過程中的記憶體分配情況的。模板中一個叫(分配)Allocations,以及一個被稱為VM Tracker(虛擬機器跟蹤)。Allocations可以幫助我們檢視全域性記憶體使用情況(Overall Memory Use): 從全域性的角度監測應用程式的記憶體使用情況,捕捉非預期的或大幅度的記憶體增長。
1.檢測記憶體不合理引用
重複操作記憶體是否持續增長,每次操作後,點選mark generations button,會設定一個flag,然後檢視每個迭代的詳細資料
Allocations.png
2.選擇Detail的Allocation List,可以檢視擷取的某一時間段內的記憶體分配情況
3.選擇Call Tree 右側設定
settings
Separate by Thread: 每個執行緒應該分開考慮。只有這樣你才能揪出那些大量佔用CPU的"重"執行緒
Invert Call Tree: 從上倒下跟蹤堆疊,這意味著你看到的表中的方法,將已從第0幀開始取樣,這通常你是想要的,只有這樣你才能看到CPU中話費時間最深的方法.也就是說FuncA{FunB{FunC}} 勾選此項後堆疊以C->B-A 把呼叫層級最深的C顯示在最外面
Hide System Libraries: 勾選此項你會顯示你app的程式碼,這是非常有用的. 因為通常你只關心cpu花在自己程式碼上的時間不是系統上的
Flatten Recursion: 遞迴函式, 每個堆疊跟蹤一個條目
三、檢測記憶體洩漏 Leaks
記憶體洩漏使用Leaks檢測,如果物件發生記憶體洩漏,detail panel 中會看到物件的retain release歷史記錄,如果非物件發生記憶體洩漏,就會看到malloc和free的呼叫歷史。
1.選中Leaks Checks,在Details所在欄中選擇CallTree
leak1
2.Call Tree會給我們大概的位置,有時候會給我們精確的位置,選中出現記憶體洩漏的區域,縮小範圍,篩選資料。
leak2
3.且在右下 Display Settings 中勾選 Invert Call Tree 和 Hide System Libraries 或其他選項可以過濾顯示的資料。
leak3
4.在導航欄的篩選框中,我們可以輸入關鍵字來篩選資料。
leak4
四、 查詢野指標 Zombies
在開啟ARC後,可以很大程度上避免產生EXC_BAD_ACCESS錯誤,但也是有出現可能的,比如非NSObject物件的產生的野指標。
1.使用Zombies工具,啟動Zombies後在內部設定了NSZombieEnabled為True。
啟用了NSZombieEnabled的話,它會用一個殭屍來替換預設的dealloc實現,也就是在引用計數降到0時,該殭屍實現會將該物件轉換成殭屍物件。殭屍物件的作用是在你向它傳送訊息時,就不會向之前那樣Crash或者產生 一個難以理解的行為,而是放出一個錯誤訊息,它會顯示一段日誌並自動跳入偵錯程式, 因此我們就可以找到具體或者大概是哪個物件被錯誤的釋放了。
基本上通過檢視Zombies工具給出的資訊找出錯誤程式碼行是比較簡單的,Zombies也只有在產生EXC_BAD_ACCESS錯誤時才有用。
zombies
2.XCode也提供了手動設定NSZombieEnabled環境變數的方法,不過設定NSZombieEnabled為True後,會導致記憶體佔用的增長,同時會影響Leaks工具的除錯,這是因為設定NSZombieEnabled會用殭屍物件來代替已釋放物件。
文/starkShen(簡書作者)
原文連結:http://www.jianshu.com/p/2ed69864ea02
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。