iOS/OS X 藉助工具解決內存問題

25.jpg

上 一篇博客iOS/OS X內存管理(一):基本概念與原理主要講了iOS/OSX 內存管理中引用計數和內存管理規則,以及引入ARC新的內存管理機制以後如何選擇ownership qualifiers(__strong、__weak、__unsafe_unretained和__autoreleasing)來管理內存。這篇我 們主要關注在實際開發中會遇到哪些內存管理問題,以及如何使用工具來調試和解決。html

166109-00c90f0f030c3665.png

在往下看以前請下載實例MemoryProblems,咱們將以這個工程展開如何檢查和解決內存問題。ios

懸掛指針問題git

懸掛指針(Dangling Pointer)就是當指針指向的對象已經釋放或回收後,但沒有對指針作任何修改(通常來講,將它指向空指針),而是仍然指向原來已經回收的地址。若是指針指向的對象已經釋放,但仍然使用,那麼就會致使程序crash。github

當你運行MemoryProblems後,點擊懸掛指針那個選項,就會出現EXC_BAD_ACCESS崩潰信息。api

166109-14751cda6424d749.png

咱們看看這個NameListViewController是作什麼的?它繼承UITableViewController,主要顯示多個名字的信息。它的實現文件以下:工具

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

要想經過tableView顯示數據,首先要實現UITableViewDataSource這個協議,爲了瘦身 controller和複用data source,我將它分離到一個類ArrayDataSource來實現UITableViewDataSource這個協議。而後在 viewDidLoad方法裏面將dataSource賦值給tableView.dataSource。ui

解釋完NameListViewController的職責後,接下來咱們須要思考出現EXC_BAD_ACCESS錯誤的緣由和位置信息。atom

通常來講,出現EXC_BAD_ACCESS錯誤的緣由都是懸掛指針致使的,但具體是哪一個指針是懸掛指針還不肯定,由於控制檯並無給出具體crash信息。spa

啓用NSZombieEnabled

要想獲得更多的crash信息,你須要啓動NSZombieEnabled。具體步驟以下:

1.選中Edit Scheme,並點擊

166109-f4e0337f766e1e89.png

2.Run -> Diagnostics -> Enable Zombie Objects

166109-ae4f6b55212b75a9.png

設置完以後,再次運行和點擊懸掛指針,雖然會再次crash,但此次控制檯打印瞭如下有用信息:

166109-9fe90d621bf6ce06.png

信息message sent to deallocated instance 0x7fe19b081760大意是向一個已釋放對象發送信息,也就是已釋放對象還調用某個方法。如今咱們大概知道什麼緣由致使程序會crash,可是具體哪一個對象被釋放還仍然使用呢?

點擊上面紅色框的Continue program execution按鈕繼續運行,截圖以下:

166109-654444b25d8c5155.png

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

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

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

而assign對應就是__unsafe_unretained,它跟__weak類似,被它修飾的變量都不持有對象的全部權,但當變量指向的對象的RC爲0時,變量並不設置爲nil,而是繼續保存對象的地址。

所以,在viewDidLoad方法中

 

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.tableView.dataSource = self.dataSource;    
    /*  因爲dataSource是被assign修飾,self.dataSource賦值後,它對象的對象就立刻釋放,
     *  而self.tableView.dataSource也不是strong,而是weak,此時仍然使用,全部會致使程序crash
     */
}

分析完緣由和定位錯誤代碼後,至於如何修改,我想你們都心知肚明瞭,若是還不知道的話,留言給我。

內存泄露問題

還記得上一篇iOS/OS X內存管理(一):基本概念與原理的引用循環例子嗎?它會致使內存泄露,上次只是文字描述,不怎麼直觀,此次咱們嘗試使用Instruments裏面的子工具Leaks來檢查內存泄露。

靜態分析

通常來講,在程序未運行以前咱們能夠先經過Clang Static Analyzer(靜態分析)來檢查代碼是否存在bug。好比,內存泄露、文件資源泄露或訪問空指針的數據等。下面有個靜態分析的例子來說述如何啓用靜態分析以及靜態分析可以查找哪些bugs。

啓動程序後,點擊靜態分析,立刻就出現crash

166109-036d86de2b9e9424.png

此時,即便啓用NSZombieEnabled,控制檯也不能打印出更多有關bug的信息,具體緣由是什麼,等下會解釋。

打開StaticAnalysisViewController,裏面引用Facebook Infer工具的代碼例子,包含我的平常開發中會出現的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

下面咱們經過靜態分析來檢查代碼是否存在bugs。有兩個方式:

  • 手動靜態分析:每次都是經過點擊菜單欄的Product -> Analyze或快捷鍵shift + command + b

166109-a890797a4457159d.png

  • 自動靜態分析:在Build Settings啓用Analyze During 'Build',每次編譯時都會自動靜態分析

166109-5c1dcdd871fcb891.png

靜態分析結果以下:

166109-6c032a57f0fef09b.png

通 過靜態分析結果,咱們來分析一下爲何NSZombieEnabled不能定位EXC_BAD_ACCESS的錯誤代碼位置。因爲callback傳入進 來的是null指針,而NSZombieEnabled只能針對某個已經釋放對象的地址,因此啓動NSZombieEnabled是不能定位的,不過能夠 經過靜態分析可得知。

啓動Instruments

有時使用靜態分析可以檢查出一些內存泄露問題,可是有時只有運行時使用Instruments才能檢查到,啓動Instruments步驟以下:

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

166109-95b4ea305007d321.png

2.此時,出現Instruments的工具集,選中Leaks子工具點擊

166109-379b199e81584b16.png

3.打開Leaks工具以後,點擊紅色圓點按鈕啓動Leaks工具,在Leaks工具啓動同時,模擬器或真機也跟着啓動

166109-03e04393903c0c6d.png

4.啓動Leaks工具後,它會在程序運行時記錄內存分配信息和檢查是否發生內存泄露。當你點擊引用循環進去那個頁面後,再返回到主頁,就會發生內存泄露

166109-1148d40299015b5f.gif

內存泄露.gif

QQ截圖20160217175300.png

若是發生內存泄露,咱們怎麼定位哪裏發生和爲何會發生內存泄露?

定位內存泄露

藉助Leaks能很快定位內存泄露問題,在這個例子中,步驟以下:

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

45.png

  • 而後雙擊某行內存泄露調用棧,會直接跳到內存泄露代碼位置

46.png

分析內存泄露緣由

上面已經定位好內存泄露代碼的位置,至於緣由是什麼?能夠查看上一篇的iOS/OS X內存管理(一):基本概念與原理的循環引用例子,那裏已經有詳細的解釋。

難以檢測Block引用循環

大 多數的內存問題均可以經過靜態分析和Instrument Leak工具檢測出來,可是有種block引用循環是難以檢測的,看咱們這個Block內存泄露例子,跟上面的懸掛指針例子差很少,只是在 configureCellBlock裏面調用一個方法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");
}

咱們首先用靜態分析來看看能不能檢查出內存泄露:

166109-c9f8a4c970462eb6.png

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

166109-68e795cea155fd8e.gif

結果跟使用靜態分析同樣,仍是沒有任何內存泄露信息的提示。

那麼咱們怎麼知道這個BlockLeakViewController發生了內存泄露呢?仍是根據iOS/OS X內存管理機制的一個基本原理:當某個對象的引用計數爲0時,它就會自動調用- (void)dealloc方法。

在 這個例子中,若是BlockLeakViewController被navigationController pop出去後,沒有調用dealloc方法,說明它的某個屬性對象仍然被持有,未被釋放。而我在dealloc方法打印release BlockLeakViewController信息:

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

在我點擊返回按鈕後,其並無打印出來,所以這個BlockLeakViewController存在內存泄露問題的。至於如何解決block內存泄露這個問題,不少基本功紮實的同窗都知道如何解決,不懂的話,本身查資料解決吧!

總結

一 般來講,在建立工程的時候,我都會在Build Settings啓用Analyze During 'Build',每次編譯時都會自動靜態分析。這樣的話,寫完一小段代碼以後,就立刻知道是否存在內存泄露或其餘bug問題,而且能夠修bugs。而在運 行過程當中,若是出現EXC_BAD_ACCESS,啓用NSZombieEnabled,看出現異常後,控制檯可否打印出更多的提示信息。若是想在運行時 查看是否存在內存泄露,使用Instrument Leak工具。可是有些內存泄露是很難檢查出來,有時只有經過手動覆蓋dealloc方法,看它最終有沒有調用。

 

 

轉自:http://www.cocoachina.com/ios/20160222/15333.html

相關文章
相關標籤/搜索