1. 程式人生 > >iOS開發之效能除錯Instruments(二)

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
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。