Instruments工具使用辦法

這篇我們主要關注在實際開發(fā)中會遇到哪些內(nèi)存管理問題,以及如何使用工具來調(diào)試和解決。
本文demo下載地址

懸掛指針問題

懸掛指針(Dangling Pointer)就是當指針指向的對象已經(jīng)釋放或回收后,但沒有對指針做任何修改(一般來說,將它指向空指針),而是仍然指向原來已經(jīng)回收的地址。如果指針指向的對象已經(jīng)釋放,但仍然使用,那么就會導致程序Crash。

當你運行MemoryProblems后,點擊懸掛指針那個選項,就會出現(xiàn)EXC_BAD_ACCESS崩潰信息:

屏幕快照

我們看看這個NameListViewController是做什么的?它繼承UITableViewController,主要顯示多個名字的信息。它的實現(xiàn)文件如下:

#import "NameListViewController.h"
#import "ArrayDataSource.h"

static NSString *const kNameCellIdentifier = @"NameCell";

@interface NameListViewController ()

#pragma mark - Model
@property (strong, nonatomic) NSArray *nameList;

#pragma mark - Data source
@property (weak, nonatomic) ArrayDataSource *dataSource;

@end

@implementation NameListViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.tableView.dataSource = self.dataSource;
}

#pragma mark - Lazy initialization
- (NSArray *)nameList
{
    if (!_nameList) {
        _nameList = @[@"Sam", @"Mike", @"John", @"Paul", @"Jason"];
    }
    return _nameList;
}

- (ArrayDataSource *)dataSource
{
    if (!_dataSource) {
        _dataSource = [[ArrayDataSource alloc] initWithItems:self.nameList
 cellIdentifier:kNameCellIdentifier tableViewStyle:UITableViewCellStyleDefault configureCellBlock:^(UITableViewCell *cell, NSString *item, NSIndexPath *indexPath) {
            cell.textLabel.text = item;
        }];
    }
    return _dataSource;
}

@end

要想通過tableView顯示數(shù)據(jù),首先要實現(xiàn)UITableViewDataSource這個協(xié)議,為了瘦身controller和復用data source,我將它分離到一個類ArrayDataSource來實現(xiàn)UITableViewDataSource這個協(xié)議。然后在viewDidLoad方法里面將dataSource賦值給tableView.dataSource。

解釋完NameListViewController的職責后,接下來我們需要思考出現(xiàn)EXC_BAD_ACCESS錯誤的原因和位置信息。

一般來說,出現(xiàn)EXC_BAD_ACCESS錯誤的原因都是懸掛指針導致的,但具體是哪個指針是懸掛指針還不確定,因為控制臺并沒有給出具體Crash信息。

啟用NSZombieEnabled

要想得到更多的Crash信息,你需要啟動NSZombieEnabled。具體步驟如下:

(1). 選中Edit Scheme,并點擊:

564d8bcdea

(2). Run -> Diagnostics -> Enable Zombie Objects

564d8c0719c26_middle.jpg

設置完之后,再次運行和點擊懸掛指針,雖然會再次Crash,但這次控制臺打印了以下有用信息:

564d8d4c5ea07_middle.jpg

信息message sent to deallocated instance 0x7fe19b081760大意是向一個已釋放對象發(fā)送信息,也就是已釋放對象還調(diào)用某個方法?,F(xiàn)在我們大概知道什么原因?qū)е鲁绦驎rash,但是具體哪個對象被釋放還仍然使用呢?

點擊上面紅色框的Continue program execution按鈕繼續(xù)運行,截圖如下

564d8fbae8454_middle.jpg

留意上面的兩個紅色框,它們兩個地址是一樣,而且ArrayDataSource前面有個NSZombie修飾符,說明dataSource對象被釋放還仍然使用。

再進一步看dataSource聲明屬性的修飾符是assign:

屏幕快照

而assign對應就是__unsafe_unretained,它跟__weak相似,被它修飾的變量都不持有對象的所有權,但當變量指向的對象的RC為0時,變量并不設置為nil,而是繼續(xù)保存對象的地址。

因此,在viewDidLoad方法中

- (void)viewDidLoad {  
    [super viewDidLoad];  
  
    self.tableView.dataSource = self.dataSource;      
    /*  由于dataSource是被assign修飾,self.dataSource賦值后,它對象的對象就馬上釋放, 
     *  而self.tableView.dataSource也不是strong,而是weak,此時仍然使用,所有會導致程序crash 
     */  
}

分析完原因和定位錯誤代碼后,至于如何修改,我想大家都心知肚明了,如果還不知道的話,留言給我。
內(nèi)存泄露問題

還記得上一篇《iOS/OS X內(nèi)存管理(一):基本概念與原理》的引用循環(huán)例子嗎?它會導致內(nèi)存泄露,上次只是文字描述,不怎么直觀,這次我們嘗試使用Instruments里面的子工具Leaks來檢查內(nèi)存泄露。

靜態(tài)分析

一般來說,在程序未運行之前我們可以先通過Clang Static Analyzer(靜態(tài)分析)來檢查代碼是否存在bug。比如,內(nèi)存泄露、文件資源泄露或訪問空指針的數(shù)據(jù)等。下面有個靜態(tài)分析的例子來講述如何啟用靜態(tài)分析以及靜態(tài)分析能夠查找哪些bugs。

啟動程序后,點擊靜態(tài)分析,馬上就出現(xiàn)Crash:

屏幕快照 2016-12-22 下午1.06.59.png

此時,即使啟用NSZombieEnabled,控制臺也不能打印出更多有關bug的信息,具體原因是什么,等下會解釋。

打開StaticAnalysisViewController,里面引用Facebook Infer工具的代碼例子,包含個人日常開發(fā)中會出現(xiàn)的Bugs:

@implementation StaticAnalysisViewController  
  
#pragma mark - Lifecycle  
- (void)viewDidLoad  
{  
    [super viewDidLoad];  
  
    [self memoryLeakBug];  
    [self resoureLeakBug];  
    [self parameterNotNullCheckedBlockBug:nil];  
    [self npeInArrayLiteralBug];  
    [self prematureNilTerminationArgumentBug];  
}  
  
#pragma mark - Test methods from facebook infer iOS Hello examples  
- (void)memoryLeakBug  
{  
     CGPathRef shadowPath = CGPathCreateWithRect(self.inputView.bounds, NULL);  
}  
  
- (void)resoureLeakBug  
{  
    FILE *fp;  
    fp=fopen("info.plist", "r");  
}  
  
-(void) parameterNotNullCheckedBlockBug:(void (^)())callback {  
    callback();  
}  
  
-(NSArray*) npeInArrayLiteralBug {  
    NSString *str = nil;  
    return @[@"horse", str, @"dolphin"];  
}  
  
-(NSArray*) prematureNilTerminationArgumentBug {  
    NSString *str = nil;  
    return [NSArray arrayWithObjects: @"horse", str, @"dolphin", nil];  
}  
  
@end

下面我們通過靜態(tài)分析來檢查代碼是否存在Bugs。有兩個方式:

(1)手動靜態(tài)分析:每次都是通過點擊菜單欄的Product -> Analyze或快捷鍵Shift + Command + B

564d92bb234b2_middle.jpg

(2). 自動靜態(tài)分析:在Build Settings啟用Analyze During 'Build',每次編譯時都會自動靜態(tài)分析

564d92eaa240e_middle.jpg

靜態(tài)分析結果如下

564d937614022_middle.jpg

通過靜態(tài)分析結果,我們來分析一下為什么NSZombieEnabled不能定位EXC_BAD_ACCESS的錯誤代碼位置。由于callback傳入進來的是null指針,而NSZombieEnabled只能針對某個已經(jīng)釋放對象的地址,所以啟動NSZombieEnabled是不能定位的,不過可以通過靜態(tài)分析可得知。

啟動Instruments

有時使用靜態(tài)分析能夠檢查出一些內(nèi)存泄露問題,但是有時只有運行時使用Instruments才能檢查到,啟動Instruments步驟如下:

1). 點擊Xcode的菜單欄的Product -> Profile啟動Instruments

564d93b3109a2_middle.jpg

2). 此時,出現(xiàn)Instruments的工具集,選中Leaks子工具點擊:

564d934752f52_middle.jpg

3). 打開Leaks工具之后,點擊紅色圓點按鈕啟動Leaks工具,在Leaks工具啟動同時,模擬器或真機也跟著啟動:

564d948a10f0b_middle.jpg

4). 啟動Leaks工具后,它會在程序運行時記錄內(nèi)存分配信息和檢查是否發(fā)生內(nèi)存泄露。當你點擊引用循環(huán)進去那個頁面后,再返回到主頁,就會發(fā)生內(nèi)存泄露:

166109-1148d40299015b5f.gif
564d951e1aa6a_middle.jpg

如果發(fā)生內(nèi)存泄露,我們怎么定位哪里發(fā)生和為什么會發(fā)生內(nèi)存泄露?

定位內(nèi)存泄露

166109-e157b6d2c837daa2.png

借助Leaks能很快定位內(nèi)存泄露問題,在這個例子中,步驟如下:

首先點擊Leak Checks時間條那個紅色叉

166109-02b6904da2ff94f9.png

然后雙擊某行內(nèi)存泄露調(diào)用棧,會直接跳到內(nèi)存泄露代碼位置

166109-e157b6d2c837daa2.png

分析內(nèi)存泄露原因

上面已經(jīng)定位好內(nèi)存泄露代碼的位置,至于原因是什么?可以查看上一篇的《iOS/OS X內(nèi)存管理(一):基本概念與原理》的循環(huán)引用例子,那里已經(jīng)有詳細的解釋。

難以檢測Block引用循環(huán)

大多數(shù)的內(nèi)存問題都可以通過靜態(tài)分析和Instrument Leak工具檢測出來,但是有種block引用循環(huán)是難以檢測的,看我們這個Block內(nèi)存泄露例子,跟上面的懸掛指針例子差不多,只是在configureCellBlock里面調(diào)用一個方法configureCell。

- (ArrayDataSource *)dataSource  
{  
    if (!_dataSource) {  
        _dataSource = [[ArrayDataSource alloc] initWithItems:self.nameList  
                                              cellIdentifier:kNameCellIdentifier  
                                              tableViewStyle:UITableViewCellStyleDefault  
                                          configureCellBlock:^(UITableViewCell *cell, NSString *item, NSIndexPath *indexPath) {  
                                              cell.textLabel.text = item;  
  
                                              [self configureCell];  
                                          }];  
    }  
    return _dataSource;  
}  
  
- (void)configureCell  
{  
    NSLog(@"Just for test");  
}  
- (void)dealloc  
{  
    NSLog(@"release BlockLeakViewController");  
}

我們首先用靜態(tài)分析來看看能不能檢查出內(nèi)存泄露

166109-c9f8a4c970462eb6.png

結果是沒有任何內(nèi)存泄露的提示,我們再用Instrument Leak工具在運行時看看能不能檢查出:

166109-68e795cea155fd8e.gif

結果跟使用靜態(tài)分析一樣,還是沒有任何內(nèi)存泄露信息的提示。

那么我們怎么知道這個BlockLeakViewController發(fā)生了內(nèi)存泄露呢?還是根據(jù)iOS/OS X內(nèi)存管理機制的一個基本原理:當某個對象的引用計數(shù)為0時,它就會自動調(diào)用- (void)dealloc方法。

在這個例子中,如果BlockLeakViewController被navigationController pop出去后,沒有調(diào)用dealloc方法,說明它的某個屬性對象仍然被持有,未被釋放。而我在dealloc方法打印release BlockLeakViewController信息

- (void)dealloc  
{  
    NSLog(@"release BlockLeakViewController");  
} 

在我點擊返回按鈕后,其并沒有打印出來,因此這個BlockLeakViewController存在內(nèi)存泄露問題的。至于如何解決block內(nèi)存泄露這個問題,很多基本功扎實的同學都知道如何解決,不懂的話,自己查資料解決吧!
其實對block進行內(nèi)存泄漏的檢測主要的循環(huán)引用問題,這里可以使用Facebook的第三方框架FBRetainCycleDetector來進行檢測,具體使用方法見下面鏈接https://github.com/facebook/FBRetainCycleDetector

總結

一般來說,在創(chuàng)建工程的時候,我都會在Build Settings啟用Analyze During 'Build',每次編譯時都會自動靜態(tài)分析。這樣的話,寫完一小段代碼之后,就馬上知道是否存在內(nèi)存泄露或其他bug問題,并且可以修bugs。而在運行過程中,如果出現(xiàn)EXC_BAD_ACCESS,啟用NSZombieEnabled,看出現(xiàn)異常后,控制臺能否打印出更多的提示信息。如果想在運行時查看是否存在內(nèi)存泄露,使用Instrument Leak工具。但是有些內(nèi)存泄露是很難檢查出來,有時只有通過手動覆蓋dealloc方法,看它最終有沒有調(diào)用。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容