iOS/OS X內存管理(二):借助工具解決內存問題

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

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

懸掛指針問題

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

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

我們看看這個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

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

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

啟用NSZombieEnabled

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

  1. 選中Edit Scheme,并點擊

  2. Run -> Diagnostics -> Enable Zombie Objects


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

信息message sent to deallocated instance 0x7fe19b081760大意是向一個已釋放對象發送信息,也就是已釋放對象還調用某個方法。現在我們大概知道什么原因導致程序會crash,但是具體哪個對象被釋放還仍然使用呢?

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

留意上面的兩個紅色框,它們兩個地址是一樣,而且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


此時,即使啟用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
  • 自動靜態分析:在Build Settings啟用Analyze During 'Build',每次編譯時都會自動靜態分析

靜態分析結果如下:

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

啟動Instruments

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

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

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

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

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

內存泄露.gif

如果發生內存泄露,我們怎么定位哪里發生和為什么會發生內存泄露?

定位內存泄露

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

  • 首先點擊Leak Checks時間條那個紅色叉
  • 然后雙擊某行內存泄露調用棧,會直接跳到內存泄露代碼位置


分析內存泄露原因

上面已經定位好內存泄露代碼的位置,至于原因是什么?可以查看上一篇的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");
}

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

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

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

那么我們怎么知道這個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方法,看它最終有沒有調用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容