raywenderlich寫的關於內存管理,第一篇,再說一次基礎知識點

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

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

 

 

教程截圖:node

  當我檢查其餘開發人員的代碼時,彷佛最多見的錯誤老是圍繞在以Object-C中的內存管理爲中心。若是您使用的語言是java或C#,它們會自動爲您處理內存管理,但這也會使你對於手工內存管理工做更加迷惑。所以,在本教程中,您將經過一些實踐來學習Object-C中的內存管理是如何工做的。咱們將討論引用計數如何工做,並經過學習內存管理的全部關鍵點來構建一個真實世界的例子——一個關於您喜好的壽司類型的應用程序。程序員

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

 

開始數組

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

 

 

  比方說,咱們但願在這個列表中填入咱們喜好的壽司類型。最簡單的方法是建立一個數組來容下每一種壽司類型的字符串名稱,而後每次咱們顯示一行,從數組中放入合適的字符串到表格中。在rootViewController.h中爲壽司類型聲明一個實例變量,代碼以下:安全

 

複製代碼
#import <UIKit/UIKit.h>

@interface RootViewController : UITableViewController {
NSArray * _sushiTypes;
}

@end
複製代碼

 

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

 

  也許你會奇怪,爲何咱們在命名的變量前面添加一個下劃線?這剛好是我喜歡作的事情,這樣作有些事情會變得更容易。在後續的關於Objec-C教程中我將討論我爲何喜歡這麼作,可是如今請注意,到目前爲止,咱們所做的是僅僅添加了一個實例變量,沒有作與屬性相關的東東,咱們把它命名爲「如下劃線開頭」,這只是一個我的的喜愛問題,其實它沒有作特別的東西。ide

  如今,打開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來檢測內存泄漏。所以,提早準備好跟我來吧!  

相關文章
相關標籤/搜索