我們曾經對iPhone的記憶體管理做過比較深入的報道,如何有效控制iPhone記憶體管理的對象的所有權與引用計數和以及iPhone記憶體的自動釋放與便捷方法。本文我們将介紹在iPhone應用中如何避免記憶體洩露。想了解“在iPhone應用中如何避免記憶體洩露”就必須先了解iPhone記憶體管理的所有權。
所有權是iPhone記憶體管理的核心思想,對象的所有者負責在使用完對象後進行釋放。一個對象可以有多個所有者,當它沒有所有者時将被設定為取消配置設定(deallocation)。
建立對象時,所有權通過alloc、new、或者copy的方式建立,之後通過調用retain或者通過Cocoa函數來配置設定和複制對象的所有權。記憶體釋放有兩種方式,一種方法是明确地請求釋放對象的所有權,另一種方法則是使用自動釋放池(auto-release pool)。
所有權的背後是一個和引用有關的運算系統,iPhone SDK的大多數對象使用這個系統,彼此之間建立着很強的引用和參照。
當你建立一個對象時,引用值為1,調用一次retain則對象的引用值加1,調用一次release則對象的引用值減1,當引用值為0時,對象的所有權配置設定将被取消。使用自動釋放池意味着對象的所有權将在一段延後的時間内被自動取消。
對象之間也可以建立弱的引用參照,此時意味着,引用值不會被保留,對象的配置設定需要手動取消。
什麼時候使用retain?
什麼時候你想阻止對象在使用前就被釋放?
每當使用copy、alloc、retain、或者Cocoa函數來建立和複制所有權,你都需要相應的release或者auto-release。
開發者應該從所有權的角度來考慮對象,而不必擔心引用值。隻要你有相應的retain和release方法,就能夠對引用值進行+1和-1操作。
注意:你或許想使用[object retainCount],但它可能因為SDK的底層代碼而發生傳回值出錯的情況。在記憶體管理時不推薦這種方式。
将對象設定為自動釋放意味着不需要明确地請求釋放,因為當自動釋放池清空時它們将被自動釋放。iPhone在主線程上運作自動釋放池,能夠在事件循環結束後釋放對象。當你建立你自己的線程時,你需要建立自己的自動釋放池。
iPhone上有便利的構造函數,用這種方法建立的對象會設定為自動釋放。
例子:
1. NSString* str0 = @"hello";
2. NSString* str1 = [NSString stringWithString:@"world"];
3. NSString* str2 = str1;
一個已配置設定的對象可以用如下的方法設定為自動釋放:
NSString* str = [[NSString alloc] initWithString:@"the flash?"];
[str autorelease];
或者用下面的方法:
1. NSString* str = [[[NSString alloc] initWithString:@"batman!"] autorelease];
當指針出界,或者當自動釋放池清空時,自動釋放對象上的所有權将被取消。
在一個事件循環結束時,自動釋放池内的構件通常會被清空。但是當你的循環每次疊代都配置設定大量記憶體時,你或許希望這不要發生。這種情況下,你可以在循環内建立自動釋放池。自動釋放池可以嵌套,是以内部池清空時,其中配置設定的對象将被釋放。在下面的例子中,每次疊代後将釋放對象。
1. for (int i = 0; i < 10; ++i)
2. {
3. NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
4. NSString* str = [NSString stringWithString:@"hello world"];
5. [self ProcessMessage: str];
6. [pool drain];
7. }
注意:在編寫的時候iPhone不支援垃圾回收,是以drain和release的功能相同。當你想為程式設定OSX的端口時通常會使用drain,除非後來在iPhone中添加了垃圾回收機制。Drain能夠擊發垃圾回收器釋放記憶體。
開發者在遵循所有權規則時需要清楚哪些函數擁有對象的所有權。下面是傳回一個對象的指針并釋放的例子。
錯誤的方法:
1. - (NSMutableString*) GetOutput
3. NSMutableString* output = [[NSMutableString alloc] initWithString:@"output"];
4. return output;
5. }
6. - (void) Test
7. {
8. NSMutableString* obj = [self GetOutput];
9. NSLog(@"count: %d", [obj retainCount]);
10. [obj release];
11. }
在這個例子中,output 的所有者是 GetOutput,讓 Test 釋放 obj 違反了Coccoa記憶體管理指南中的規則,盡管它不會洩露記憶體但是這樣做不好,因為Test 不應該釋放并非它所擁有的對象。
正确的方法:
- (NSMutableString*) GetOutput
{
NSMutableString* output = [[NSMutableString alloc] nitWithString:@"output"];
return [output autorelease];
}
- (void) Test
NSMutableString* obj = [self GetOutput];
NSLog(@"count: %d", [obj retainCount]);
在第二個例子中,output 被設定為當 GetOutput 傳回時自動釋放。output的引用值減少,GetObject 釋放 output 的所有權。Test 函數現在可以自由的 retain 和 release 對象,請確定它不會洩露記憶體。
例子中 obj 被設定為自動釋放,是以 Test 函數沒有它的所有權,但是如果它需要在其他地方存儲對象會怎樣?
此時對象需要有一個新的所有者來保留。
setter函數必須保留它所存儲的對象,也就是聲明所有權。如果我們想要建立一個 setter 函數,我們需要在配置設定一個新的指向成員變量的指針之前做兩件事情。
在函數裡:
- (void) setName:(NSString*)newName
首先我們要減少成員變量的引用值:
[name release];
這将允許當引用值為0時 name 對象被釋放,但是它也允許對象的其他所有者繼續使用對象。
然後我們增加新的 NSString 對象的引用值:
[newName retain];
是以當 setName 結束時, newName 不會被取消配置設定。 newName 現在指向的對象和 name 指向的對象不同,兩者有不同的引用值。
現在我們設定 name 指向 newName 對象:
name = newName;
但是如果 name 和 newName 是同一個對象時怎麼辦?我們不能在它被釋放後保留它,并再次釋放。
在釋放存儲的對象前保留新的對象:
1. [newName retain];
2. [name release];
3. name = newName;
現在兩個對象是相同的,先增加它的引用值,然後再減少,進而使得指派前引用值不變。
另一種做法是使用 objective-c:
聲明如下:
@property(nonatomic, retain) NSString *name;
1. nonatomic 表示沒有對同一時間擷取資料的多個線程進行組塊兒。Atomic 為一個單一的線程鎖定資料,但因為 atomic 的方式比較緩慢,是以不是必須的情況一般不使用。
2. retain 表示我們想要保留 newName 對象。
我們可以使用 copy代替 retain:
@property(nonatomic, copy) NSString *name;
這和下面的函數一樣:
1. - (void) setName:(NSString*)newName
3. NSString* copiedName = [newName copy];
4. [name release];
5. name = copiedName;
6. [name retain];
7. [copiedName release];
8. }
newName 在這裡被複制到 copiedName,現在 copiedName 擁有串的一個副本。name 被釋放,而 copiedName 被賦給 name。之後 name 保留這個串,進而使得 copiedName 和 name 同時擁有它。最後 copiedName 釋放這個對象,name 成為這個串的唯一所有者。
如果我們有如下的函數,像這樣的 setters 将被輸入用來保留成員對象:
1. - (void) Test
3. NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
4. // do something...
5. name = [self GetOutput];
6. // do something else...
7. NSLog(@"Client Name before drain: %@", name);
8. [pool drain];
9. NSLog(@"Client Name after drain: %@", name);
10. }
name 在調用至 drain 後是未定義的,因為當池被釋放時,name 也将被釋放。
如果我們用如下的部分替代指派:
[self setName:[self GetOutput]];
然後 name 将被這個類所有,在使用時保留直到調用 release
那麼我們何時釋放對象?
由于 name 是成員變量,釋放它的最安全的辦法是對它所屬的類使用 dealloc 函數。
1. - (void)dealloc
3. [name release];
4. [super dealloc];
注意:雖然并不總是調用 dealloc,依靠 dealloc 來釋放對象可能是危險,可能會觸發一些想不到的事情。在出口處,iPhone OS 可能在調用 dealloc 前清空全部應用程式的記憶體。
當用 setter 給對象指派時,請小心下面的語句:
[self setName:[[NSString alloc] init]];
name 的設定是正确的但 alloc 沒有相應的釋放,下面的方式要好一些:
1. NSString* s = [[NSString alloc] init];
2. [self setName:s];
3. [s release];
或者使用自動釋放:
[self setName:[[[NSString alloc] init] autorelease]];
自動釋放池釋放位于配置設定和 drain 函數之間的對象。
我們在下面的函數中設定一個循環,在循環中将 NSNumber 的一個副本賦給 magicNumber,另外将 magicNumber 設定為自動釋放。在這個例子中,我們希望在每次疊代時清空自動釋放池(這樣可以在指派的數量很大時節省循環的記憶體)
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 在本地的自動釋放池中被指派和釋放,是以當外部的池清空時,clientName 已經被釋放了,任何對 clientName 的進一步使用都是沒有定義的。
在這個例子中,我們在指派後保留 clientName,直到結束時再釋放它:
3. NSString* clientName = nil;
4. NSNumber* magicNumber = nil;
5. for (int i = 0; i < 10; ++i)
6. {
7. NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
8. magicNumber = [[self GetMagicNumber] copy];
9. [magicNumber autorelease];
10. if (i == [magicNumber intValue])
11. {
12. clientName = [self GetOutput];
13. [clientName retain];
14. }
15. [pool drain];
16. }
17. if (clientName != nil)
18. {
19. NSLog(@"Client Name: %@", clientName);
20. [clientName release];
21. }
22. }
我們在調用 retain 函數和 release 函數的期間獲得 clientName 的所有權。通過添加一對 retain 和 release 的調用,我們就確定 clientName 在明确調用釋放前不會被自動釋放。
當一個對象被添加進集合時,它就被集合所擁有。
在這個例子中我們配置設定一個串,它現在有了所有者;
1. NSString* str = [[NSString alloc] initWithString:@"Bruce Wayne"];
然後我們将它添加進數組,現在它有兩個所有者:
1. [array addObject: str];
我們可以安全的釋放這個串,使其僅被數組所有:
[str release];
當一個集合被釋放時,其中的所有對象都将被釋放。
1. NSMutableArray* array = [[NSMutableArray alloc] init];
2. NSString* str = [[NSString alloc] initWithString:@"Bruce Wayne"];
3. [array addObject: str];
4. [array release];
在上面的例子中,我們配置設定了一個數組和一個串,然後将串添加到數組中并釋放數組。這使得串僅擁有一個所有者,并且在我們調用 [str release] 前它不會被釋放。
在這個函數中,我們從串的 input 傳遞到函數 DoSomething,然後釋放 input
3. NSMutableString* input = [[NSMutableString alloc] initWithString:@"batman!"];
4. [NSThread detachNewThreadSelector:@selector(DoSomething:) toTarget:self withObject:input];
5. [input release];
6. }
detatchNewThreadSelector 增加 input 對象的引用值并線上程結束時釋放它。這就是為什麼我們能夠線上程剛開始的時候就釋放 input,而無論函數 DoSomething 何時開始或結束。
1. - (void) DoSomething:(NSString*)str
3. [self performSelectorOnMainThread:@selector(FinishSomething:) withObject:str waitUntilDone:false];
4. }
performSeclectorOnMainThread 也會保留傳遞的對象,直到 selector 結束。
自動釋放池是特殊的線程,是以如果我們在一個新的線程上建立自動釋放的對象,我們需要建立一個自動釋放池來釋放它們。
[NSThread detachNewThreadSelector:@selector(Process) toTarget:self withObject:nil];
這裡在另一個線程上調用函數 Process
1. - (void) Process
4. NSMutableString* output = [[[NSMutableString alloc] initWithString:@"batman!"] autorelease];
5. NSLog(@"output: %@", output);
6. [self performSelectorOnMainThread:@selector(FinishProcess) withObject:nil waitUntilDone:false];
7. [pool drain];
對象 output 被配置設定并且在自動釋放池中設定了自動釋放,它将在函數結束前被釋放。
1. - (void) FinishProcess
3. NSMutableString* output = [[[NSMutableString alloc] initWithString:@"superman?"] autorelease];
4. NSLog(@"output: %@", output);
系統會為主線程自動建立一個自動釋放池,是以在 FinishProcess 中,我們不需要為主線程上運作的函數建立自動釋放池。
為了在你的iPhone中避免記憶體洩露,你必須要清楚每個被配置設定對象的所有者是誰,要明白什麼時候釋放所有權,并且還要始終按對設定 retain 和 release,這三點非常重要。如果你遵循所有權的規則,你的應用将更加穩定并且因為 bug 的減少而節省大量時間。