在iPhone應(yīng)用中如何避免內(nèi)存泄露
我們?cè)?jīng)對(duì)iPhone的內(nèi)存管理做過比較深入的報(bào)道,如何有效控制iPhone內(nèi)存管理的對(duì)象的所有權(quán)與引用計(jì)數(shù)和以及iPhone內(nèi)存的自動(dòng)釋放與便捷方法。本文我們將介紹在iPhone應(yīng)用中如何避免內(nèi)存泄露。想了解“在iPhone應(yīng)用中如何避免內(nèi)存泄露”就必須先了解iPhone內(nèi)存管理的所有權(quán)。
關(guān)于所有權(quán)
所有權(quán)是iPhone內(nèi)存管理的核心思想,對(duì)象的所有者負(fù)責(zé)在使用完對(duì)象后進(jìn)行釋放。一個(gè)對(duì)象可以有多個(gè)所有者,當(dāng)它沒有所有者時(shí)將被設(shè)置為取消分配(deallocation)。
創(chuàng)建對(duì)象時(shí),所有權(quán)通過alloc、new、或者copy的方式建立,之后通過調(diào)用retain或者通過Cocoa函數(shù)來分配和復(fù)制對(duì)象的所有權(quán)。 內(nèi)存釋放有兩種方式,一種方法是明確地請(qǐng)求釋放對(duì)象的所有權(quán),另一種方法則是使用自動(dòng)釋放池(auto-release pool)。
所有權(quán)的背后是一個(gè)和引用有關(guān)的運(yùn)算系統(tǒng),iPhone SDK的大多數(shù)對(duì)象使用這個(gè)系統(tǒng),彼此之間建立著很強(qiáng)的引用和參照。
當(dāng)你創(chuàng)建一個(gè)對(duì)象時(shí),引用值為1,調(diào)用一次retain則對(duì)象的引用值加1,調(diào)用一次release則對(duì)象的引用值減1,當(dāng)引用值為0時(shí),對(duì)象的所有權(quán)分配將被取消。使用自動(dòng)釋放池意味著對(duì)象的所有權(quán)將在一段延后的時(shí)間內(nèi)被自動(dòng)取消。
對(duì)象之間也可以建立弱的引用參照,此時(shí)意味著,引用值不會(huì)被保留,對(duì)象的分配需要手動(dòng)取消。
什么時(shí)候使用retain?
什么時(shí)候你想阻止對(duì)象在使用前就被釋放?
每當(dāng)使用copy、alloc、retain、或者Cocoa函數(shù)來創(chuàng)建和復(fù)制所有權(quán),你都需要相應(yīng)的release或者auto-release。
開發(fā)者應(yīng)該從所有權(quán)的角度來考慮對(duì)象,而不必?fù)?dān)心引用值。只要你有相應(yīng)的retain和release方法,就能夠?qū)σ弥颠M(jìn)行+1和-1操作。
注意:你或許想使用[object retainCount],但它可能因?yàn)镾DK的底層代碼而發(fā)生返回值出錯(cuò)的情況。在內(nèi)存管理時(shí)不推薦這種方式。
自動(dòng)釋放
將對(duì)象設(shè)置為自動(dòng)釋放意味著不需要明確地請(qǐng)求釋放,因?yàn)楫?dāng)自動(dòng)釋放池清空時(shí)它們將被自動(dòng)釋放。iPhone在主線程上運(yùn)行自動(dòng)釋放池,能夠在事件循環(huán)結(jié)束后釋放對(duì)象。當(dāng)你創(chuàng)建你自己的線程時(shí),你需要?jiǎng)?chuàng)建自己的自動(dòng)釋放池。
iPhone上有便利的構(gòu)造函數(shù),用這種方法創(chuàng)建的對(duì)象會(huì)設(shè)置為自動(dòng)釋放。
例子:
- NSString* str0 = @"hello";
- NSString* str1 = [NSString stringWithString:@"world"];
- NSString* str2 = str1;
一個(gè)已分配的對(duì)象可以用如下的方法設(shè)置為自動(dòng)釋放:
- NSString* str = [[NSString alloc] initWithString:@"the flash?"];
- [str autorelease];
或者用下面的方法:
- NSString* str = [[[NSString alloc] initWithString:@"batman!"] autorelease];
當(dāng)指針出界,或者當(dāng)自動(dòng)釋放池清空時(shí),自動(dòng)釋放對(duì)象上的所有權(quán)將被取消。
在一個(gè)事件循環(huán)結(jié)束時(shí),自動(dòng)釋放池內(nèi)的構(gòu)件通常會(huì)被清空。但是當(dāng)你的循環(huán)每次迭代都分配大量內(nèi)存時(shí),你或許希望這不要發(fā)生。這種情況下,你可以在循 環(huán)內(nèi)創(chuàng)建自動(dòng)釋放池。自動(dòng)釋放池可以嵌套,所以內(nèi)部池清空時(shí),其中分配的對(duì)象將被釋放。在下面的例子中,每次迭代后將釋放對(duì)象。
- for (int i = 0; i < 10; ++i)
- {
- NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
- NSString* str = [NSString stringWithString:@"hello world"];
- [self ProcessMessage: str];
- [pool drain];
- }
注意:在編寫的時(shí)候iPhone不支持垃圾回收,所以drain和release的功能相同。當(dāng)你想為程序設(shè)置OSX的端口時(shí)通常會(huì)使用drain,除非后來在iPhone中添加了垃圾回收機(jī)制。Drain能夠擊發(fā)垃圾回收器釋放內(nèi)存。
返回一個(gè)對(duì)象的指針
開發(fā)者在遵循所有權(quán)規(guī)則時(shí)需要清楚哪些函數(shù)擁有對(duì)象的所有權(quán)。下面是返回一個(gè)對(duì)象的指針并釋放的例子。
錯(cuò)誤的方法:
- - (NSMutableString*) GetOutput
- {
- NSMutableString* output = [[NSMutableString alloc] initWithString:@"output"];
- return output;
- }
- - (void) Test
- {
- NSMutableString* obj = [self GetOutput];
- NSLog(@"count: %d", [obj retainCount]);
- [obj release];
- }
在這個(gè)例子中,output 的所有者是 GetOutput,讓 Test 釋放 obj 違反了Coccoa內(nèi)存管理指南中的規(guī)則,盡管它不會(huì)泄露內(nèi)存但是這樣做不好,因?yàn)門est 不應(yīng)該釋放并非它所擁有的對(duì)象。
正確的方法:
- - (NSMutableString*) GetOutput
- {
- NSMutableString* output = [[NSMutableString alloc] initWithString:@"output"];
- return [output autorelease];
- }
- - (void) Test
- {
- NSMutableString* obj = [self GetOutput];
- NSLog(@"count: %d", [obj retainCount]);
- }
在第二個(gè)例子中,output 被設(shè)置為當(dāng) GetOutput 返回時(shí)自動(dòng)釋放。output的引用值減少,GetObject 釋放 output 的所有權(quán)。Test 函數(shù)現(xiàn)在可以自由的 retain 和 release 對(duì)象,請(qǐng)確保它不會(huì)泄露內(nèi)存。
例子中 obj 被設(shè)置為自動(dòng)釋放,所以 Test 函數(shù)沒有它的所有權(quán),但是如果它需要在其他地方存儲(chǔ)對(duì)象會(huì)怎樣?
此時(shí)對(duì)象需要有一個(gè)新的所有者來保留。
Setters
setter函數(shù)必須保留它所存儲(chǔ)的對(duì)象,也就是聲明所有權(quán)。如果我們想要?jiǎng)?chuàng)建一個(gè) setter 函數(shù),我們需要在分配一個(gè)新的指向成員變量的指針之前做兩件事情。
在函數(shù)里:
- - (void) setName:(NSString*)newName
首先我們要減少成員變量的引用值:
- [name release];
這將允許當(dāng)引用值為0時(shí) name 對(duì)象被釋放,但是它也允許對(duì)象的其他所有者繼續(xù)使用對(duì)象。
然后我們?cè)黾有碌?NSString 對(duì)象的引用值:
- [newName retain];
所以當(dāng) setName 結(jié)束時(shí), newName 不會(huì)被取消分配。 newName 現(xiàn)在指向的對(duì)象和 name 指向的對(duì)象不同,兩者有不同的引用值。
現(xiàn)在我們?cè)O(shè)置 name 指向 newName 對(duì)象:
- name = newName;
但是如果 name 和 newName 是同一個(gè)對(duì)象時(shí)怎么辦?我們不能在它被釋放后保留它,并再次釋放。
在釋放存儲(chǔ)的對(duì)象前保留新的對(duì)象:
- [newName retain];
- [name release];
- name = newName;
現(xiàn)在兩個(gè)對(duì)象是相同的,先增加它的引用值,然后再減少,從而使得賦值前引用值不變。
另一種做法是使用 objective-c:
聲明如下:
- @property(nonatomic, retain) NSString *name;
1. nonatomic 表示沒有對(duì)同一時(shí)間獲取數(shù)據(jù)的多個(gè)線程進(jìn)行組塊兒。Atomic 為一個(gè)單一的線程鎖定數(shù)據(jù),但因?yàn)?atomic 的方式比較緩慢,所以不是必須的情況一般不使用。
2. retain 表示我們想要保留 newName 對(duì)象。
我們可以使用 copy 代替 retain:
- @property(nonatomic, copy) NSString *name;
這和下面的函數(shù)一樣:
- - (void) setName:(NSString*)newName
- {
- NSString* copiedName = [newName copy];
- [name release];
- name = copiedName;
- [name retain];
- [copiedName release];
- }
newName 在這里被復(fù)制到 copiedName,現(xiàn)在 copiedName 擁有串的一個(gè)副本。name 被釋放,而 copiedName 被賦給 name。之后 name 保留這個(gè)串,從而使得 copiedName 和 name 同時(shí)擁有它。*** copiedName 釋放這個(gè)對(duì)象,name 成為這個(gè)串的唯一所有者。
如果我們有如下的函數(shù),像這樣的 setters 將被輸入用來保留成員對(duì)象:
- - (void) Test
- {
- NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
- // do something...
- name = [self GetOutput];
- // do something else...
- NSLog(@"Client Name before drain: %@", name);
- [pool drain];
- NSLog(@"Client Name after drain: %@", name);
- }
name 在調(diào)用至 drain 后是未定義的,因?yàn)楫?dāng)池被釋放時(shí),name 也將被釋放。
如果我們用如下的部分替代賦值:
- [self setName:[self GetOutput]];
然后 name 將被這個(gè)類所有,在使用時(shí)保留直到調(diào)用 release
那么我們何時(shí)釋放對(duì)象?
由于 name 是成員變量,釋放它的最安全的辦法是對(duì)它所屬的類使用 dealloc 函數(shù)。
- - (void)dealloc
- {
- [name release];
- [super dealloc];
- }
注意:雖然并不總是調(diào)用 dealloc,依靠 dealloc 來釋放對(duì)象可能是危險(xiǎn),可能會(huì)觸發(fā)一些想不到的事情。在出口處,iPhone OS 可能在調(diào)用 dealloc 前清空全部應(yīng)用程序的內(nèi)存。
當(dāng)用 setter 給對(duì)象賦值時(shí),請(qǐng)小心下面的語句:
- [self setName:[[NSString alloc] init]];
name 的設(shè)置是正確的但 alloc 沒有相應(yīng)的釋放,下面的方式要好一些:
- NSString* s = [[NSString alloc] init];
- [self setName:s];
- [s release];
或者使用自動(dòng)釋放:
- [self setName:[[[NSString alloc] init] autorelease]];
自動(dòng)釋放池
自動(dòng)釋放池釋放位于分配和 drain 函數(shù)之間的對(duì)象。
我們?cè)谙旅娴暮瘮?shù)中設(shè)置一個(gè)循環(huán),在循環(huán)中將 NSNumber 的一個(gè)副本賦給 magicNumber,另外將 magicNumber 設(shè)置為自動(dòng)釋放。在這個(gè)例子中,我們希望在每次迭代時(shí)清空自動(dòng)釋放池(這樣可以在賦值的數(shù)量很大時(shí)節(jié)省循環(huán)的內(nèi)存)
- - (void) Test
- {
- NSString* clientName = nil;
- NSNumber* magicNumber = nil;
- for (int i = 0; i < 10; ++i)
- {
- NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
- magicNumber = [[self GetMagicNumber] copy];
- [magicNumber autorelease];
- if (i == [magicNumber intValue])
- {
- clientName = [self GetOutput];
- }
- [pool drain];
- }
- if (clientName != nil)
- {
- NSLog(@"Client Name: %@", clientName);
- }
- }
這里存在的問題是 clientName 在本地的自動(dòng)釋放池中被賦值和釋放,所以當(dāng)外部的池清空時(shí),clientName 已經(jīng)被釋放了,任何對(duì) clientName 的進(jìn)一步使用都是沒有定義的。
在這個(gè)例子中,我們?cè)谫x值后保留 clientName,直到結(jié)束時(shí)再釋放它:
- - (void) Test
- {
- NSString* clientName = nil;
- NSNumber* magicNumber = nil;
- for (int i = 0; i < 10; ++i)
- {
- NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
- magicNumber = [[self GetMagicNumber] copy];
- [magicNumber autorelease];
- if (i == [magicNumber intValue])
- {
- clientName = [self GetOutput];
- [clientName retain];
- }
- [pool drain];
- }
- if (clientName != nil)
- {
- NSLog(@"Client Name: %@", clientName);
- [clientName release];
- }
- }
我們?cè)谡{(diào)用 retain 函數(shù)和 release 函數(shù)的期間獲得 clientName 的所有權(quán)。通過添加一對(duì) retain 和 release 的調(diào)用,我們就確保 clientName 在明確調(diào)用釋放前不會(huì)被自動(dòng)釋放。
集合
當(dāng)一個(gè)對(duì)象被添加進(jìn)集合時(shí),它就被集合所擁有。
在這個(gè)例子中我們分配一個(gè)串,它現(xiàn)在有了所有者;
- NSString* str = [[NSString alloc] initWithString:@"Bruce Wayne"];
然后我們將它添加進(jìn)數(shù)組,現(xiàn)在它有兩個(gè)所有者:
- [array addObject: str];
我們可以安全的釋放這個(gè)串,使其僅被數(shù)組所有:
- [str release];
當(dāng)一個(gè)集合被釋放時(shí),其中的所有對(duì)象都將被釋放。
- NSMutableArray* array = [[NSMutableArray alloc] init];
- NSString* str = [[NSString alloc] initWithString:@"Bruce Wayne"];
- [array addObject: str];
- [array release];
在上面的例子中,我們分配了一個(gè)數(shù)組和一個(gè)串,然后將串添加到數(shù)組中并釋放數(shù)組。這使得串僅擁有一個(gè)所有者,并且在我們調(diào)用 [str release] 前它不會(huì)被釋放。
用線程傳遞指針
在這個(gè)函數(shù)中,我們從串的 input 傳遞到函數(shù) DoSomething,然后釋放 input
- - (void) Test
- {
- NSMutableString* input = [[NSMutableString alloc] initWithString:@"batman!"];
- [NSThread detachNewThreadSelector:@selector(DoSomething:) toTarget:self withObject:input];
- [input release];
- }
detatchNewThreadSelector 增加 input 對(duì)象的引用值并在線程結(jié)束時(shí)釋放它。這就是為什么我們能夠在線程剛開始的時(shí)候就釋放 input,而無論函數(shù) DoSomething 何時(shí)開始或結(jié)束。
- - (void) DoSomething:(NSString*)str
- {
- [self performSelectorOnMainThread:@selector(FinishSomething:) withObject:str waitUntilDone:false];
- }
performSeclectorOnMainThread 也會(huì)保留傳遞的對(duì)象,直到 selector 結(jié)束。
自動(dòng)釋放池是特殊的線程,所以如果我們?cè)谝粋€(gè)新的線程上創(chuàng)建自動(dòng)釋放的對(duì)象,我們需要?jiǎng)?chuàng)建一個(gè)自動(dòng)釋放池來釋放它們。
- [NSThread detachNewThreadSelector:@selector(Process) toTarget:self withObject:nil];
這里在另一個(gè)線程上調(diào)用函數(shù) Process
- - (void) Process
- {
- NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
- NSMutableString* output = [[[NSMutableString alloc] initWithString:@"batman!"] autorelease];
- NSLog(@"output: %@", output);
- [self performSelectorOnMainThread:@selector(FinishProcess) withObject:nil waitUntilDone:false];
- [pool drain];
- }
對(duì)象 output 被分配并且在自動(dòng)釋放池中設(shè)置了自動(dòng)釋放,它將在函數(shù)結(jié)束前被釋放。
- - (void) FinishProcess
- {
- NSMutableString* output = [[[NSMutableString alloc] initWithString:@"superman?"] autorelease];
- NSLog(@"output: %@", output);
- }
系統(tǒng)會(huì)為主線程自動(dòng)創(chuàng)建一個(gè)自動(dòng)釋放池,所以在 FinishProcess 中,我們不需要為主線程上運(yùn)行的函數(shù)創(chuàng)建自動(dòng)釋放池。
總結(jié)
為了在你的iPhone中避免內(nèi)存泄露,你必須要清楚每個(gè)被分配對(duì)象的所有者是誰,要明白什么時(shí)候釋放所有權(quán),并且還要始終按對(duì)設(shè)置 retain 和 release,這三點(diǎn)非常重要。如果你遵循所有權(quán)的規(guī)則,你的應(yīng)用將更加穩(wěn)定并且因?yàn)?bug 的減少而節(jié)省大量時(shí)間。
【編輯推薦】