iOS:內存管理(一):OC中的內存管理


 

前言:

之前iOS的項目大多是有使用StroryBoard以及ARC的,iOS推出的這兩個特性確實帶來了一些好處。StoryBoard讓界面跳轉邏輯更清楚,也可以將一些功能模塊獨立復用。而ARC則從手動管理內存的麻煩,可以更專注於程序邏輯、架構與設計模式等。但是,這兩個特性還是蠻有爭議的,也存在不少坑,至使外面很多iOS開發的直接拋棄他們。首先是StroryBoard,像xib一樣,最大的坑就是版本管理的問題。像我以往的項目,都是自己獨立開發的,問題不大,但項目一大,團隊合作的話少不了版本管理,但是你一打開,不小心動一動,StroryBoard就被修改了,這對版本管理無疑是災難。另外就是使用StroryBoard得更大的內存消耗。而對於ARC,很多人的顧慮一是擔心技術不成熟,自己管理更放心;二是擔心由非ARC遷移到ARC可能會有風險;三是認為ARC只支持iOS5及其以上版本。其實我是較認同ARC技術的,手動管理管理不善的話跟容易泄露,而我做的項目中使用ARC完全沒有問題;非ARC到ARC的問題,apple體用的可以針對特定文件使用ARC,問題應該也不大;再來ARC其實也可以支持4.3的版本,不過要做一些修改。

綜上,在多人團隊協作中,StroryBoard確實不宜采用,但是ARC則是相對成熟的了,其實可以使用,減輕開發成本,縮短開發周期。

但是,使用ARC也不是意味這對其原本的手動管理完全不用了解,加上很大部分公司仍沒有擁抱ARC,故有接下來《iOS:內存管理》的內容。轉載自 子龍山人 博客的譯文,原文來自http://www.raywenderlich.com/。

 

(譯)Object-C中的內存管理

原文鏈接地址:http://www.raywenderlich.com/2657/memory-management-in-objective-c-tutorial

  免責申明(必讀!):本博客提供的所有教程的翻譯原稿均來自於互聯網,僅供學習交流之用,切勿進行商業傳播。同時,轉載時不要移除本申明。如產生任何糾紛,均與本博客所有人、發表該翻譯稿之人無任何關系。謝謝合作!

  注:本教程由北方和我本人合作翻譯。

教程截圖:

     當我檢查其他開發人員的代碼時,似乎最常見的錯誤總是圍繞在以Object-C中的內存管理為中心。如果您使用的語言是java或C#,它們會自動為您處理內存管理,但這也會使你對於手工內存管理工作更加迷惑。因此,在本教程中,您將通過一些實踐來學習Object-C中的內存管理是如何工作的。我們將討論引用計數如何工作,並通過學習內存管理的所有關鍵點來構建一個真實世界的例子——一個關於您喜愛的壽司類型的應用程序。

  本教程是針對初學者的iOS開發人員或者時關注這個主題的中級開發人員。廢話就少啰嗦了,開始編碼。

 開始

  在xcode開發環境中,打開File\New Project,選擇iOS\Application\Navigation-based Application,並將新項目命名為ProMemFun,執行Build\Build and Run, 在模擬器中你會看到一個如下空表視圖:

 

  比方說,我們希望在這個列表中填入我們喜愛的壽司類型。最簡單的方法是創建一個數組來容下每一種壽司類型的字符串名稱,然后每次我們顯示一行,從數組中放入合適的字符串到表格中。在rootViewController.h中為壽司類型聲明一個實例變量,代碼如下:

#import <UIKit/UIKit.h>
 
@interface RootViewController : UITableViewController {
    NSArray * _sushiTypes;
}
 
@end 

  通過這個聲明,每個RootViewController實例對象將有空間來存儲一個指向NSArray數組的指針,這是一個Object-C類,使用這個數組初始化后就不能改變它。如果你需要更改一個初始化后的數組(例如,添加一項后),你應該使用NSMutableArray替代。 

  也許你會奇怪,為什么我們在命名的變量前面添加一個下划線?這恰好是我喜歡做的事情,這樣做有些事情會變得更容易。在后續的關於Objec-C教程中我將討論我為什么喜歡這么做,但是現在請注意,到目前為止,我們所作的是僅僅添加了一個實例變量,沒有做與屬性相關的東東,我們把它命名為“以下划線開頭”,這只是一個個人的喜好問題,其實它沒有做特別的東西。

  現在,打開RootViewController.m文件,注釋viewDiaLoad,然后設置以下代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
 
    _sushiTypes = [[NSArray alloc] initWithObjects:@"California Roll", 
                   @"Tuna Roll", @"Salmon Roll", @"Unagi Roll", 
                   @"Philadelphia Roll", @"Rainbow Roll",
                   @"Vegetable Roll", @"Spider Roll", 
                   @"Shrimp Tempura Roll", @"Cucumber Roll",
                   @"Yellowtail Roll", @"Spicy Tuna Roll",
                   @"Avocado Roll", @"Scallop Roll",
                   nil];
} 

 

  現在我們進入內存管理,Object-C中創建的對象使用的是引用計數。這就意味着每一個對象都跟蹤有多少其他的對象引用它。一旦引用計數變為0,這個對象的內存就會安全的釋放掉。

  作為一個程序員,你要確保對象的引用計數總是准確的。當你在某個地方存儲了一個對象的指針(比如是實例變量),你需要增加引用計數,有時候需要遞減引用計數。

“我的天啊”,你可能會思考,“這聽起來太復雜和混亂了”,不要擔心,做起來要比聽起來簡單些。

 

初始化對象和釋放對象的內存

  不管什么時候你在Object-c中創建一個對象,首先你要調用alloc為這個對象去分配內存空間,然后調用init方法去初始化這個對象,當init方法不帶任何參數時,有時候你會看到程序員用new方法替代(這類似於先調用alloc,然后調用init)。

  最重要的是一旦你這么做了,你會得到一個新的對象,並且它的引用計數置為1。因此,當完成所有的工作后,你需要遞減引用計數。

好了,我們給出一個開頭。仍然是在RootViewController.m中,去文件末尾,像下面一樣設置viewDidUnload和dealloc方法:

- (void)viewDidUnload {
    [_sushiTypes release];
    _sushiTypes = nil;
}
 
- (void)dealloc {
    [_sushiTypes release];
    _sushiTypes = nil;
    [super dealloc];
} 

 

  記住當你用alloc/init創建一個array時,它的引用計數已經為1了。因此當你完成與array相關的工作時,需要遞減它的引用計數。在Object-C中,你可以通過對這個對象調用release方法。

  但是你應該在什么地方release呢?哦,你一定要在dealloc方法中release這個array,顯然易見,當這個viewController銷毀后,你也不會再需要這個array了。所以,記住無論何時你在viewDidLoad中創建一個對象(這個對象的引用計數會初始化為1),你應該在viewDidUnload中釋放這個對象。不要太擔心,關於這兒主題我會專門寫一篇教程。

  注意,釋放對象后,請將其設置為nil,如果你試圖調用一個指向nil的指針,你的程序會崩潰。

  好了,現在讓我們使用新的array。首先,替換掉tableView:numberOfRowsInSection 里面的"return 0",替換成下面的語句:

// Replace "return 0;" in tableView:numberOfRowsInSection with this
return _sushiTypes.count; 

  這里意思是說,tableView里面的數據行數等於sushiTypes數組里面的記錄個數。

  現在,我們需要告訴table view,每一行具體顯示什么內容。找到tableView:cellForRowAtIndexPath函數,然后找到注釋 “Configure the cell”,在后面添加下列代碼:

 

NSString * sushiName = [_sushiTypes objectAtIndex:indexPath.row]; // 1
NSString * sushiString = 
    [[NSString alloc] initWithFormat:@"%d: %@", 
        indexPath.row, sushiName]; // 2
cell.textLabel.text = sushiString; // 3
[sushiString release]; // 4 

 

  讓我們一行一行代碼解釋一下上面的程序:

  1. 根據當前行號查找sushiTypes數組里面對應的字符串
  2. 我們想這樣顯示字符串:“3: Unagi Roll“,3代表行號,而“Unagi Roll” 是那一行的sushi的名字。要構建一個具有這種格式的字符串的話,你可以用NSString的initWithFormat來輕松構建。記住,當你這樣做完之后,返回的字符串的引用計數是1.
  3. 設置當前行的文本為剛剛得到的格式化字符串。當你這樣設置之后,text label會把sushiString copy一下。(相應的,其引用計數會加1)
  4. 我們用完sushiString了,因此,調用release把它釋放掉。如果你忘了這樣做的話,那么這里就會導致一個內存泄漏。因為字符串的引用計數是1,永遠也不會得到釋放。(即使text label把sushiString釋放了一次,也沒用。因為剛開始創建的時候是1,賦值的時候為2,然后再label再釋放一次,為1。而如果你不調用[sushiString release]的話,那么就會內存泄漏)

  編譯並運行,如果一切OK的話,你將會看到sushi的列表。

Autorelease Your Potential

  目前為止,你知道了,當你調用alloc/init的時候,引用計數是1,當你用完這個對象的時候,你需要調用release把引用計數變為0.

  接下來,讓我們討論一下另外一種方法----autorelease。

  當你給一個對象發送autorelease消息后,它的意思是說“嘿!我想讓你在將來某個時刻被釋放掉,比如當前run loop結束的時候。但是,現在我能夠使用你”。

  最容易理解的方式就是看代碼。修改 tableView:cellForRowAtIndexPath 方法,找到 “Configure the cell”注釋,在后面添加下列代碼:

NSString * sushiName = [_sushiTypes objectAtIndex:indexPath.row]; // 1
NSString * sushiString = 
    [[[NSString alloc] initWithFormat:@"%d: %@", 
        indexPath.row, sushiName] autorelease]; // 2
cell.textLabel.text = sushiString; // 3 

  因此,和上一次相比,這里只改了兩個地方。首先,你在第二行結尾的時候調用了autorelease。其次,你把最后一行release的調用代碼移除掉了。

  接上來,我解釋一下。在第2行代碼結束的時候,sushiString的引用計數是1,但是,我們給它發送了一個autorelease消息。這意味着,你可以在這個函數里面使用sushiString,但是,一旦下一次run loop被調用的時候,它就會被發送release對象。然后引用計數改為0,那么內存也就被釋放掉了。(關於autorelease到底是怎么工作的,我的理解是:每一個線程都有一個autoreleasePool的棧,里面放了很多autoreleasePool對象。當你向一個對象發送autorelease消息之后,就會把該對象加到當前棧頂的autoreleasePool中去。當當前runLoop結束的時候,就會把這個pool銷毀,同時對它里面的所有的autorelease對象發送release消息。而autoreleasePool是在當前runLoop開始的時候創建的,並壓入棧頂。那么什么是一個runLoop呢?一個UI事件,Timer call, delegate call, 都會是一個新的Runloop。)

  在這個例子中,上面的解決辦法非常好,但是,后面我們不會使用它。然而,如果我們想要存儲一個變量(但是不retain它),然后在某個地方使用這個變量(比如用戶點擊某一行的時候,選中那一行),那么我們就有大麻煩了。因為那樣我們是在嘗試訪問一個已經銷毀的對象,可想而知,程序肯定是crash拉!

  有時候,當你調用一些方法的時候,你得到的返回給你的對象的引用計數是1,但是,它是一個autorelease的對象。你修改一下tableView:cellForRowAtIndexPath方法,修改成下面的樣子,然后你就知道我剛剛講的是什么意思了:

NSString * sushiName = [_sushiTypes objectAtIndex:indexPath.row]; // 1
NSString * sushiString = 
    [NSString stringWithFormat:@"%d: %@", 
        indexPath.row, sushiName]; // 2
cell.textLabel.text = sushiString; // 3 

  這里代碼改變之處是第2行。你不是自己調用 alloc/init/autorelease,而是使用NSString的一個類方法stringWithFormat。這個方法會返回一個引用計數為1的字符串,並且它是一個autorelease的對象。因此,和上面的寫法一樣,你可以放心的使用這個字符串,但是,如果你不retain它,然后又在后面某個地方使用它的話,那么程序就會崩潰。

  你可能會奇怪,你怎么知道哪些對象返回給你的時候是autorelease的?好吧,讓我教你一個簡單的慣用法,具體如下:

  • 如果一個方法以init或者copy開頭,那么返回給你的對象的引用計數是1,並且這不是一個autorelease的對象。換句話說,你調用這些方法的話,你就對返回的對象負責,你再用完之后必須手動調用release來釋放內存。
  • 如果一個方法不是以init或者copy開頭的話,那么返回的對象引用計數為1,但是,這是一個autorelease對象。換句話說,你現在可以放心使用此對象,用完之后它會自動釋放內存。但是,如果你想在其它地方使用它(比如換個函數),那么,這時,你就需要手動retain它了。

Retain Your Wits

  如果你現在有一個autorelease對象,並且像在后面繼續使用它,那么該怎么辦呢?其實很簡單,你只需要對它發送retain消息就OK了。這樣會把引用計數變為2,但是,只要出了當前runLoop,那么引用計數又會變為1,那么對象還是不會銷毀(因為只有引用計數為0才能銷毀)。

  讓我們來看看具體怎么做。打開RootViewController.h ,然后在@interface里面添加一個實例變量:

NSString * _lastSushiSelected; 

  這里只是定義了一個新的實例變量,它將用來追蹤選中的最后那一行的字符串。

  接下來,修改 tableView:didSelectRowAtIndexPath ,修改如下:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
 
    NSString * sushiName = [_sushiTypes objectAtIndex:indexPath.row]; // 1
    NSString * sushiString = [NSString stringWithFormat:@"%d: %@", 
        indexPath.row, sushiName]; // 2
 
    NSString * message = [NSString stringWithFormat:@"Last sushi: %@.  Cur sushi: %@", _lastSushiSelected, sushiString]; // 3
    UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:@"Sushi Power!" 
                                                         message:message 
                                                        delegate:nil 
                                               cancelButtonTitle:nil 
                                               otherButtonTitles:@"OK", nil] autorelease]; // 4
    [alertView show]; // 5
 
    [_lastSushiSelected release]; // 6
    _lastSushiSelected = [sushiString retain]; // 7
 
} 

  這里的代碼比較多,讓我們一行一行來看:

  1. 查找當前行對應的shshiTypes數組里面的字符串。
  2. 根據當前行號構建一個新的字符串。注意,這里使用的是stringWithFormat方法,它返回的是一個autorelease的字符串。因為這個方法並不是以init或者copy開頭,所以你就知道。記住,這意味着,你可以在這個函數里面使用此字符串,但是出了這個函數的話,如果你還想繼續使用之,那必須要對它發送一個retain消息。
  3. 構建一個消息,用來顯示當前選中的sushi和最后選中的sushi。和上面一樣,這里也是使用的stringWithFormat方法,它返回的是一個autorelease對象。因為我們只想在這個函數里面使用,所以沒有retain。
  4. 創建一個alertView來顯示剛剛構建的那個消息。這里是通過alloc/init方式創建的,所以我們需要在之后再發送一個autorelease消息,這樣在出了這個函數以后,這個對象就會被釋放掉了。
  5. 顯示這個alert view。
  6. 再你設置lastSushiSelected實例變量之前,你需要先釋放當前的lastSushiSelected實例變量,如果當前實例變是已經是nil的話,也沒有關系,因上nil對象可以接收任何消息。
  7. 因為你想在這個函數之外再使用lastSushiSelected這個字符串,所以你需要retain它。

  還有一件事你不能忘記。為了保存不會有任何內存泄漏,你需要在RootViewController的dealloc方法里面調用下面方法來釋放內存:

[_lastSushiSelected release];
_lastSushiSelected = nil; 

  基本上,在dealloc方法被里面,你需要對“你負責的對象”發送release消息,並且要把它賦值為nil。

  編譯並運行,現在,當你選中一行,你就可以看到下面的屏幕輸出了。

引用計數相關參考資料

  讓我們回顧一下所學的知識:

  • 當你調用alloc/init的時候,你得到一個引用計數是1的對象。
  • 當你用完這個對象之后,你要對它調用release消息,使其引用計數為0,這樣它的內存才會被釋放掉。
  • 當你調用一個方法,它不是以init或者copy開頭的,這時,返回給你的對象是autorelease的,它是一種在將來某個時刻會自動被釋放的對象。(這里我也要提醒大家一句,比如你在寫一個函數,它的名字是xxx,沒有以init或者copy開頭,那么記得你返回的對象一定要是autorelease的,否則,別人在使用你這個函數的時候就會把它當前是autorelease的,那么他就不會release它,這樣就會造成內存泄漏,千萬要切記!!!)
  • 如果你想繼續使用autorelease對象,那么你就要給它放送一個retain消息。
  • 如果你使用alloc/init方法創建了一個對象,但是你想讓它自己在出了runLoop之后被自動釋放的話,那么你可以在alloc/init之后再調用autorelease。這也是一種見得比較多的寫法了。比如,cocos2d里面調用[xxx node]的時候,就等於[[[xxx alloc] init]autorelease].

  本教程只講述了objc內存管理的很基本的部分,如果想獲得更多的信息,請參考蘋果的文檔: Memory Management Programming Guide.

何去何從?

  這里有本教程的完整源代碼

  不管你是一個多么優秀的開發者,或者你對內存管理的理解有多么的深入,你還是不可避免地要犯一些內存相關的錯誤。因此,在我的下一篇教程中,我將教大家如果使用XCode, Instruments, 和 Zombies來檢測內存泄漏。因此,提前准備好跟我來吧!  

  

著作權聲明:本文由http://www.cnblogs.com/andyque翻譯,歡迎轉載分享。請尊重作者勞動,轉載時保留該聲明和作者博客鏈接,謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM