1. 程式人生 > 其它 >iOS效能優化之記憶體分析

iOS效能優化之記憶體分析

成功之前我們要做應該做的事情,成功之後我們才可以做喜歡做的事情。

  從蘋果的開發者文件裡可以看到記憶體分類如下所示,其中 Leaked memoryAbandoned memory 都屬於應該釋放而沒釋放的記憶體,都是記憶體洩露(該釋放的記憶體沒有釋放)。

1.Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).

2.Abandoned memory: Memory still referenced by your application that has no useful purpose.

3.Cached memory: Memory still referenced by your application that might be used again for better performance.

一. 記憶體分析方法

1. 靜態分析方法(Analyze)

1.1)Analyze簡介

  Clang Static Analyzer是一款靜態程式碼掃描工具,專門用於針對C,C++和Objective-C的程式進行分析。已經被Xcode整合,可以直接使用Xcode進行靜態程式碼掃描分析,也可以單獨在命令列下使用並提供html格式的輸出報吿和xml格式的結果檔案方便整合到Jenkins上進行展示。

  工具:Product->Analyze

  Analyze主要分析以下四種問題:

  • 記憶體管理錯誤檢查(Memory Error),例如記憶體洩漏等;
  • 邏輯錯誤檢查(Logic Error):訪問空指標或未初始化的變數等;
  • 宣告錯誤檢查(Dead Store):也叫無用邏輯儲存,其指永遠不會被訪問的變數、永遠不會執行的程式碼;
  • Api呼叫錯誤檢查(API Misuse)

  宣告錯誤、邏輯錯誤、Api呼叫錯誤基本在編譯時都會有警告,Analyze的主要優勢在於靜態分析記憶體洩漏及程式碼邏輯錯誤。

  點選Analyze後,Xcode會自動進行編譯分析,需要一段時間,之後會像提示警告一樣,提示有多少分析的結果。所有的分析結果按照如上的類別,歸類顯示。點選某個錯誤的地方,會定位到出錯的地方,然後點擊向上向下的箭頭,會詳細展示出出錯的步驟。

1.2)記憶體管理錯誤檢查:Memory Error

  開發的過程中,容易給宣告非空的物件賦值nil,導致出錯。包括:nil 賦值給了一個期望非空值的指標、Null賦值給非空物件,返回了 nil 值,期望返回一個非空值。

#pragma mark - Memory Error
- (void)MemoryErrorTest {
    // nil賦值給了一個期望非空值的指標 nonnull
    _userInfo = nil;
}

// 返回了nil值,期望返回一個非空值
- (NSArray * _Nonnull)returnValue {
    return nil;
}

  Analyze結果:

  記憶體洩露 Memory (Core Foundation/Objective-C):

  一般來說都是由於使用的CoreFoundation後沒有release造成的。在RAC下Foundation框架下的不需要進行release,CoreFoundation框架下仍然需要release。比如在開啟arc的環境下,輸入以下一段程式碼:

// 記憶體洩漏 - 擷取部分影象
- (UIImage*)getSubImage:(unsigned long)ulUserHeader
{
    UIImage * sourceImage = [UIImage imageNamed:@"image.png"];
    CGFloat height = sourceImage.size.height;
    CGRect rect = CGRectMake(0 + ulUserHeader*height, 0, height, height);

    CGImageRef imageRef = CGImageCreateWithImageInRect([sourceImage CGImage], rect);
    UIImage* smallImage = [UIImage imageWithCGImage:imageRef];
    //CGImageRelease(imageRef);

    return smallImage;
}

  用註釋註釋掉CGImageRelease(imageRef)這行,雖然開起了arc,不過仍然會導致imageRef物件洩漏。

  Analyze結果:

  Analyze已經分析出imageRef物件有記憶體洩漏,這種情況在編譯時是無法發現的。

1.3)邏輯錯誤檢查:Logic Error

  初看這段程式碼,並沒有覺得有什麼不妥,根據字串獲得index的值。這個前提是字串一定要按照這個規則提供,如果沒有按照這個規則提供,則index就沒有值。通過Analyze分析,就檢查出來了。

#pragma mark - Logic Error
- (NSInteger)statusIndex:(NSString *)status {
    NSInteger index;
    if ([status isEqualToString:@"正常"]) {
        index = 1;
    } else if ([status isEqualToString:@"異常"]) {
        index = 2;
    } else if ([status isEqualToString:@"嚴重"]) {
        index = 3;
    }
    return index;
}

  Analyze結果:

1.4)宣告錯誤檢查:Dead Store

  很多時候我們建立了一些中間變數需要使用,但是在最終功能的實現上並沒有用到這個變數。但是這些變數依然留在程式碼中,沒有刪除。這就造成了記憶體的不必要的開銷。這對這部分變數,不需要的時候就要及時的刪除。同理:建立類宣告的屬性,如果沒有用到就要及時刪除。因為建立類時,會根據類的屬性的多少建立對應的記憶體。

#pragma mark - Dead Store
- (void)deadStoreTest {
    NSDictionary *param = @{};
    NSString *stackTrace = [param objectForKey:@"key"];
    if (!stackTrace) {
        stackTrace = @"";
    }
    // 整個流程stackTrace只是被賦值,沒有被使用
}

  Analyze結果:

1.5)Api呼叫錯誤檢查:API Misuse(Apple)

  API的錯誤,一般是在大段的邏輯處理中沒有注意OC的使用細節。如:陣列不能新增空值,陣列的元素不能是空值,字典的value不能是空等等。下面這單程式碼:str 的初始值為空,經過一段邏輯處理後,還是有可能是空,是不能新增到陣列中的。

#pragma mark - API Misuse
- (void)addData {
    NSString *str = nil;
    long number = random();
    if (number % 3 == 0) {
        str = @"number是3的整數";
    } else if (number % 3 == 1) {
        str = @"number對3取餘為1";
    }
    NSMutableArray *dataArray = [NSMutableArray arrayWithCapacity:0];
    [dataArray addObject:str];
}

  Analyze結果:

2. 動態分析方法(Instruments)

2.1)Instruments簡介

  動態分析方法(Instruments)檢測程式在執行過程中的記憶體變化, 是Xcode自帶的工具。

  啟動Instruments:Xcode -> Product ->Profile 執行 instruments。

  由上Instruments工具面板可以看到很多分析工具,雙擊和點選choose都可以開啟除錯 ,專案中用的最多的也就是Allocations和Leaks了,也看自己的需要檢測和使用其它的工具。

2.2)Allocations(記憶體分配分析)

2.2.1)Allocations簡介

  記憶體洩漏是指記憶體被分配了,但程式中已經沒有指向該記憶體的指標,導致該記憶體無法被釋放,產生記憶體洩漏。記憶體不合理運用,蘋果官方稱這種情況為Abandoned Memory,也就是存在已分配記憶體的引用,但實際上程式中不會使用,比如圖片等物件加入了快取,但快取中的物件一直沒有被使用。

  XCode提供的Instruments中的Allocations工具可以用來幫你分析記憶體的分配情況(監測記憶體分配情況),當你的App收到記憶體警告、記憶體暴增,且持續不釋放的情況,除了是記憶體洩漏外,還有就是對效能程式碼質量不過關導致,這時首先應該用Allocations進行記憶體分析,瞭解記憶體分配情況,哪些物件佔用了太多記憶體。用真機除錯比較準確。

  在 ARC 時代更常見的記憶體洩露是迴圈引用導致的Abandoned memory,Leaks 工具查不出這類記憶體洩露,我們也可以通過Allocations記憶體分析來發現這部分記憶體洩漏。

2.2.2)Allocations幾種分析模式
  • Statistics(靜態分析)
2.2.3)如何使用Allocations
(1)準備例項程式碼

  模擬一段佔用記憶體程式碼,啟動Allocations工具監測記憶體分配情況,找到高耗記憶體的程式碼:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *identifier = @"id1";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
    cell.textLabel.text = [NSString stringWithFormat:@"%ld",indexPath.row];
    
    // 模擬佔用記憶體過高的程式碼
    for (int i = 0; i < 200; i++) {
        UIImageView *image = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"3.png"]];
        image.backgroundColor = [UIColor whiteColor];
        image.frame = CGRectMake(0, 0, 30, 30);
        [cell.contentView addSubview:image];
    }
    
    return cell;
}

  啟動Instruments -> Allocations工具

(2)Statistics靜態分析模式

  啟動Allocations預設的資料是Statistics(靜態分析)。

  為了更好的解決問題,有必要講解下這裡的記憶體相關概念:

  • All Heap Allocations:堆上malloc分配的記憶體,不包過虛擬記憶體區域;

  • All Anonymous VM:匿名的虛擬記憶體區域。何為匿名呢?就是Allocations不知道是你哪些程式碼建立的記憶體,也就是說這裡的記憶體你無法直接控制。像memory mapped file,CALayer back store等都會出現在這裡。這裡的記憶體有些是你需要優化的,有些不是;

    具體資料庫分析:

  • Graph:是否需要繪製出來記憶體曲線;

  • Category:記憶體類別;

  • Persistent:沒有釋放的記憶體大小;

  • # Persistent:沒有釋放的個數;

  • # Transient:已經釋放的個數;

  • Transient Bytes:已經釋放的記憶體大小;

  • Total Bytes:總記憶體大小(Persistent + Transient Bytes);

  • # Total:總數量(# Persistent + # Transient);

  點選Category小箭頭,進入詳情頁。選擇記憶體異常物件地址,在右側我們可以看到這個物件是如何被建立的。以及定位到程式碼。

(3)Mark Generation

  利用Generation,我們可以對記憶體的增量進行分析,時間戳B相比時間戳A有那些記憶體增加了多少,對於暴增的地方我們可以很方便發現問題。

  在記憶體異常的地方做標記:Mark Generation

  設定Call Tree選項(跟檢測記憶體洩漏是一樣)

  切換導航到Generations,檢視標記樣本

  點選標記後面的箭頭進一步分析記憶體分配,定位高記憶體程式碼塊

(4)Call Tree

  按照類似的方式,這次我們選擇Call Tree(呼叫樹)來直接分析程式碼是如何建立記憶體的。

(5)Allocations List

  Allocations List提供了一種更純粹的方式,讓你看到記憶體的分配的列表,我們一般會選擇記憶體從高到低,看看是不是有什麼意外分配的大記憶體塊

2.3)Leaks(動態分析)

2.3.1)Leaks簡介

  Leaks可以實時看到APP的記憶體洩漏,我們經常會使用到這個工具。相關工具在XCode的Product->Profile->Instruments-> Leaks或者command+I。

參考:

https://www.jianshu.com/p/4ce4e7cf6a22

2.4)Zombies(殭屍物件分析)

2.4.1)Zombies簡介

  Zombies動態分析記憶體中的殭屍物件,相關工具在XCode的Product->Profile->Instruments->Zombies。那什麼是殭屍物件呢?在使用ARC之前,很多人遇到過EXC_BAD_ACCESS錯誤,這個錯誤可以理解為訪問了已被釋放的物件,蘋果稱之為殭屍物件。

  比如在不開啟ARC下,下面這段程式碼:

NSString *str = [NSString stringWithFormat:@"HBZombie"];
NSLog(@"Go %@",str);
[str release];

  str物件不是手動分配,而是加入到自動釋放池,由釋放池負責釋放,所以第三行呼叫release時就會產生EXC_BAD_ACCESS錯誤。

  在開啟ARC後,可以很大程度上避免產生EXC_BAD_ACCESS錯誤,但也是有出現可能的,比如IOS裡使用了C++程式碼,C++部分的物件是不會有ARC來管理的。

  EXC_BAD_ACCESS錯誤不像訪問空指標一樣容易定位,往往報錯時很難查詢到錯誤點,所以XCode在Instruments中提供了單獨的Zombies工具來分析這類錯誤。

2.4.2)Zombies分析的原理

  和使用Instruments的其他工具一樣,點選XCode的Product選單Profile啟動Instruments:

  可以看到Zombies工具下邊的介紹,用於查詢那些被過度釋放的殭屍物件。

  Zombies工具的查詢原理其實和設定NSZombieEnabled環境變數的除錯方式是一樣的,啟動Zombies後在內部設定了NSZombieEnabled為True。

  啟用了NSZombieEnabled的話,它會用一個殭屍來替換預設的dealloc實現,也就是在引用計數降到0時,該殭屍實現會將該物件轉換成殭屍物件。殭屍物件的作用是在你向它傳送訊息時,就不會向之前那樣Crash或者產生 一個難以理解的行為,而是放出一個錯誤訊息,它會顯示一段日誌並自動跳入偵錯程式, 因此我們就可以找到具體或者大概是哪個物件被錯誤的釋放了。

2.4.3)使用Zombies分析的步驟

(1)啟動Instruments;

(2)在模版選擇器中,點選Zombies;

(3)選擇app和目標裝置;

(4)點選選擇建立路徑文件;

(5)點選工具欄紅色圓形按鈕或command+r開始記錄;

(6)正常使用你的app;如果一個被過度釋放的物件被訪問了,在timeline窗口裡會被插入一個標記同時殭屍物件被會話訪問出現。這表示在某個記憶體地址上一個殭屍物件被訪問了。你可以通過點選開啟和關閉Zombie Messaged Dialog(殭屍物件訪問會話);

(7)點選灰白色橫向箭頭到殭屍物件的記憶體地址並且顯示殭屍物件詳細記憶體歷史的視窗,包括相對應的引用計數和方法呼叫。

(8)在詳細視窗選擇Zombie事件(或者是其它你想研究的事件);

(9)輸入(Command+3)顯示選擇事件的棧軌跡的擴充套件詳細區域;

(10)點選Collapse按鈕在擴充套件詳細區域隱藏棧軌跡,這樣更容易看到你的應用的方法;通過使用者icon標誌Calls使你的app標記為黑色並置前;

(11)棧軌跡區雙擊方法顯示它的程式碼在Instruments中;

(12)點選Xcode按鈕在詳細視窗頂部用於開啟這程式碼在Xcode的編輯介面。

  雖然Instruments可以幫你發現“殭屍”物件,但是你仍然需要仔細檢查關係記憶體歷史來確定並解決問題。以下是常見導致殭屍物件的情況。前兩個在ARC中應該不會出現,第三個倒是極有可能。

  • release一個已經被release或者autorelease的物件;

  • 物件需要被retain時沒有被retain;

  • 一些呼叫發生在物件被release之後;

2.4.4)手動設定NSZombieEnabled環境變數

  XCode也提供了手動設定NSZombieEnabled環境變數的方法,不過設定NSZombieEnabled為True後,會導致記憶體佔用的增長,同時會影響Leaks工具的除錯,這是因為設定NSZombieEnabled會用殭屍物件來代替已釋放物件。所以一般不建議進行進行手動設定,而應該使用Zombies工具進行除錯。

  點選Product選單Edit Scheme開啟該頁面,然後勾選Zombie Objects 複選框:

2.5)Time Profiler(檢視時間佔用)

  Time Profiler還有上面介紹過的Leaks、Allocations工具,被戲稱為Instruments的救命三招,是當應用遇到問題時首先應當使用的三個工具。

  Time Profiler幫助我們分析程式碼的執行時間,找出導致程式變慢的原因,告訴我們“時間都去哪兒了?”。

  Time Profiler分析原理:它按照固定的時間間隔來跟蹤每一個執行緒的堆疊資訊,通過統計比較時間間隔之間的堆疊狀態,來推算某個方法執行了多久,並獲得一個近似值。其實從根本上來說與我們的原始分析方法異曲同工,只不過其將各個方法消耗的時間統計起來。

  選擇Time Profiler工具開始測試,這時會自動啟動模擬器和Time Profiler錄製。

  先進行一些App的操作,讓Time Profiler收集足夠的資料,尤其是你覺得那些有效能瓶頸的地方。

二.第三方檢測方法

MLeaksFinder

參考:

https://blog.csdn.net/Alpaca12/article/details/80157520

http://events.jianshu.io/p/b8d2f736ae6b

https://www.jianshu.com/p/70184408649a