03內(nèi)存管理2:借助工具解決內(nèi)存問(wèn)題

0. 引子

這篇我們主要關(guān)注在實(shí)際開(kāi)發(fā)中會(huì)遇到哪些內(nèi)存管理問(wèn)題,以及如何使用工具來(lái)調(diào)試和解決。

內(nèi)存管理

在往下看之前請(qǐng)下載實(shí)例MemoryProblems,我們將以這個(gè)工程展開(kāi)如何檢查和解決內(nèi)存問(wèn)題。

1. 懸掛指針問(wèn)題

懸掛指針(Dangling Pointer)就是當(dāng)指針指向的對(duì)象已經(jīng)釋放或回收后,但沒(méi)有對(duì)指針做任何修改(一般來(lái)說(shuō),將它指向空指針),而是仍然指向原來(lái)已經(jīng)回收的地址。如果指針指向的對(duì)象已經(jīng)釋放,但仍然使用,那么就會(huì)導(dǎo)致程序crash。

當(dāng)你運(yùn)行MemoryProblems后,點(diǎn)擊懸掛指針那個(gè)選項(xiàng),就會(huì)出現(xiàn)EXC_BAD_ACCESS
崩潰信息

Paste_Image.png

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

static NSString *const kNameCellIdentifier = @"NameCell";

@interface NameListViewController ()

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

#pragma mark - Data source@property (assign, 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

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

解釋完NameListViewController
的職責(zé)后,接下來(lái)我們需要思考出現(xiàn)EXC_BAD_ACCESS
錯(cuò)誤的原因和位置信息。
一般來(lái)說(shuō),出現(xiàn)EXC_BAD_ACCESS
錯(cuò)誤的原因都是懸掛指針導(dǎo)致的,但具體是哪個(gè)指針是懸掛指針還不確定,因?yàn)榭刂婆_(tái)并沒(méi)有給出具體crash信息。

2. 啟用NSZombieEnabled

要想得到更多的crash信息,你需要啟動(dòng)NSZombieEnabled。具體步驟如下:

2.1

選中Edit Scheme,并點(diǎn)擊

Paste_Image.png
2.2

Run -> Diagnostics -> Enable Zombie Objects

Paste_Image.png

設(shè)置完之后,再次運(yùn)行和點(diǎn)擊懸掛指針,雖然會(huì)再次crash,但這次控制臺(tái)打印了以下有用信息:

Paste_Image.png

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

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

Paste_Image.png

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

再進(jìn)一步看dataSource聲明屬性的修飾符是assign

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

而assign對(duì)應(yīng)就是__unsafe_unretained,它跟__weak相似,被它修飾的變量都不持有對(duì)象的所有權(quán),但當(dāng)變量指向的對(duì)象的RC為0時(shí),變量并不設(shè)置為nil,而是繼續(xù)保存對(duì)象的地址。

因此,在viewDidLoad方法中

- (void)viewDidLoad { 
       [super viewDidLoad]; 

       self.tableView.dataSource = self.dataSource; 
        /* 由于dataSource是被assign修飾,self.dataSource賦值后,它對(duì)象的對(duì)象就馬上釋放, * 而self.tableView.dataSource也不是strong,而是weak,此時(shí)仍然使用,所有會(huì)導(dǎo)致程序crash */
  }

分析完原因和定位錯(cuò)誤代碼后,至于如何修改,我想大家都心知肚明了,如果還不知道的話,留言給我。

3. 內(nèi)存泄露問(wèn)題

還記得上一篇iOS/OS X內(nèi)存管理(一):基本概念與原理的引用循環(huán)例子嗎?它會(huì)導(dǎo)致內(nèi)存泄露,上次只是文字描述,不怎么直觀,這次我們嘗試使用Instruments里面的子工具Leaks來(lái)檢查內(nèi)存泄露。
靜態(tài)分析
一般來(lái)說(shuō),在程序未運(yùn)行之前我們可以先通過(guò)Clang Static Analyzer(靜態(tài)分析)來(lái)檢查代碼是否存在bug。比如,內(nèi)存泄露、文件資源泄露或訪問(wèn)空指針的數(shù)據(jù)等。下面有個(gè)靜態(tài)分析的例子來(lái)講述如何啟用靜態(tài)分析以及靜態(tài)分析能夠查找哪些bugs。
啟動(dòng)程序后,點(diǎn)擊靜態(tài)分析,馬上就出現(xiàn)crash

Paste_Image.png

此時(shí),即使啟用NSZombieEnabled,控制臺(tái)也不能打印出更多有關(guān)bug的信息,具體原因是什么,等下會(huì)解釋。
打開(kāi)StaticAnalysisViewController
,里面引用Facebook Infer工具的代碼例子,包含個(gè)人日常開(kāi)發(fā)中會(huì)出現(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

下面我們通過(guò)靜態(tài)分析來(lái)檢查代碼是否存在bugs。有兩個(gè)方式:

  • 1.手動(dòng)靜態(tài)分析:每次都是通過(guò)點(diǎn)擊菜單欄的Product -> Analyze或快捷鍵shift + command + b
Paste_Image.png
  • 2.自動(dòng)靜態(tài)分析:在Build Settings啟用Analyze During 'Build',每次編譯時(shí)都會(huì)自動(dòng)靜態(tài)分析

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

Paste_Image.png
Paste_Image.png

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

4. 啟動(dòng)Instruments

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

4.1點(diǎn)擊Xcode的菜單欄的 Product -> Profile 啟動(dòng)Instruments

Paste_Image.png

4.2此時(shí),出現(xiàn)Instruments的工具集,選中Leaks子工具點(diǎn)擊

Paste_Image.png

4.3打開(kāi)Leaks工具之后,點(diǎn)擊紅色圓點(diǎn)按鈕啟動(dòng)Leaks
工具,在Leaks工具啟動(dòng)同時(shí),模擬器或真機(jī)也跟著啟動(dòng)

Paste_Image.png

4.4啟動(dòng)Leaks工具后,它會(huì)在程序運(yùn)行時(shí)記錄內(nèi)存分配信息和檢查是否發(fā)生內(nèi)存泄露。當(dāng)你點(diǎn)擊引用循環(huán)進(jìn)去那個(gè)頁(yè)面后,再返回到主頁(yè),就會(huì)發(fā)生內(nèi)存泄露

1
2.png
Paste_Image.png

5. 定位內(nèi)存泄露

如果發(fā)生內(nèi)存泄露,我們?cè)趺?strong>定位哪里發(fā)生和為什么會(huì)發(fā)生內(nèi)存泄露?
借助Leaks能很快定位內(nèi)存泄露問(wèn)題,在這個(gè)例子中,步驟如下:

  • 1.首先點(diǎn)擊Leak Checks時(shí)間條那個(gè)紅色叉
Paste_Image.png
  • 2.然后雙擊某行內(nèi)存泄露調(diào)用棧,會(huì)直接跳到內(nèi)存泄露代碼位置
Paste_Image.png

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

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

7.難以檢測(cè)Block引用循環(huán)

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

- (ArrayDataSource *)dataSource
{ 
    if (!_dataSource) { 
        _dataSource = [[ArrayDataSource alloc] initWithItems:self.nameList 
                                               cellIdentifier:kNameCellIdentifier 
                                               ctableViewStyle:UITableViewCellStyleDefault 
                                               cconfigureCellBlock:^(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)分析來(lái)看看能不能檢查出內(nèi)存泄露:

Paste_Image.png

結(jié)果是沒(méi)有任何內(nèi)存泄露的提示,我們?cè)儆肐nstrument Leak工具在運(yùn)行時(shí)看看能不能檢查出:

Paste_Image.png

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

那么我們?cè)趺粗肋@個(gè)BlockLeakViewController
發(fā)生了內(nèi)存泄露呢?還是根據(jù)iOS/OS X內(nèi)存管理機(jī)制的一個(gè)基本原理:當(dāng)某個(gè)對(duì)象的引用計(jì)數(shù)為0時(shí),它就會(huì)自動(dòng)調(diào)用- (void)dealloc
方法。
在這個(gè)例子中,如果BlockLeakViewController
被navigationController pop出去后,沒(méi)有調(diào)用dealloc
方法,說(shuō)明它的某個(gè)屬性對(duì)象仍然被持有,未被釋放。而我在dealloc
方法打印release BlockLeakViewController信息:

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

在我點(diǎn)擊返回按鈕后,其并沒(méi)有打印出來(lái),因此這個(gè)BlockLeakViewController
存在內(nèi)存泄露問(wèn)題的。至于如何解決block內(nèi)存泄露這個(gè)問(wèn)題,很多基本功扎實(shí)的同學(xué)都知道如何解決,不懂的話,自己查資料解決吧!

8.總結(jié)

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

文/Sam_Lau(簡(jiǎn)書作者)
原文鏈接:http://www.itdecent.cn/p/09c5141d4531
著作權(quán)歸作者所有,轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),并標(biāo)注“簡(jiǎn)書作者”。

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

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

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