Objective-C中內存管理的一些特例

     該教程是討論IOS平臺上內存管理規則之外的一些特殊情況,我相信大部分的開發人員可能都沒有覺察到。

    我們先普及一下Objectivie-C中的內存管理的基本知識,如果你已經比較熟悉了,可以直接跳過該節。Objective-C使用的是引用計數(Reference Counting),引用計數就是對象用一個變量來保存有幾個地方(類、方法等)在使用它。當一個對象被創造出來時,它的引用計數(下面我們用retainCount來表示這個值)爲1,在應用程序運行的過程中,可能有很多地方都用到了這個對象,凡是用到這個對象時,就將它的retainCount加1,當不用了時,再將其retainCount減1,當對象的retainCount爲0時,表示沒有人在用這個對象了,系統就會釋放這個對象所佔用的內存。

在Objective-C中,關於基於引用計數的內存管理,其實你只需要掌握三條最基本的規則:
  • 當你使用new、alloc或copy方法創建一個對象時,對象的引用計數值爲1.當不再使用該對象時,你要負責向該對象發送一條release或autorelease消息。這樣該對象會在其使用壽命結束時被銷燬。
  • 當你通過其它方法獲得一個對象時,則假設該對象的保留計數值爲1, 而且已經被設置爲自動釋放,你不需要執行任何操作來確保該對象被清理。但是如果你打算在一段時間內擁有該對象,則需要保留(引用計數加1)它,並且在操作完成時釋放它(引用計數減1)。
  • 如果你保留了某個對象,你需要最終釋放或者自動釋放該對象,必須保持保留方法(retain)和釋放方法(release/autorelease)的使用次數相等。
    只要你理解了這三條規則基本就足夠了。但是要理解下面所講的一些例外情況,僅有這些知識就不夠了。下面就讓我們開始這段探索之旅吧。
 
使用Xcode使用“IOS”-“Application”-“Single View Application”模板創建一個名爲MemoryTest的工程。在ViewController.m文件的viewDidLoad方法中鍵入下面的代碼:
  1. NSString *emptyStr = [NSString new];  
  2. NSLog(@"emptyStr retainCount: %u", emptyStr.retainCount); 
先想想你認爲輸出結果是什麼?然後看一下運行後Console的輸出是什麼?我這邊的結果是4294967295,一個很大的數,實際上這個數是UINT_MAX,就是無符號整型的最大值。你原先認爲它應該輸出1對嗎?爲什麼會這樣呢?帶着這個疑問在看下面的代碼,仍舊將其放入viewDidLoad方法中:
 
  1. NSString *emptyStr1 = [NSString new]; 
  2. NSString *emptyStr2 = [NSString new]; 
  3. NSLog(@"emptyStr1 address: %p", emptyStr1); 
  4. NSLog(@"emptyStr2 address: %p", emptyStr2); 
  5. NSLog(@"emptyStr1 retainCount: %u", emptyStr1.retainCount); 
  6. NSLog(@"emptyStr2 retainCount: %u", emptyStr2.retainCount); 
這段代碼創建了兩個新的空字符串對象,接着輸出這兩個對象的在內存中的地址和它們的引用計數值。運行一下,查看一下結果,我這邊的結果是:

       

這兩個對象的地址竟然一樣,這出離我們原先的認識對嗎?new方法兩次創建的對象竟然一樣,這在c++中是絕不可能的。但這是Objective-C,編譯器在底層做了一些我們看不見的工作。很顯然這兩個空字符串對象指針指向的是同一個對象。Objective-C爲什麼會這麼處理呢?這是因爲NSString類型的不可變性,就是這種類型的對象一旦創建,就不能改變(增加或刪除其中的字符),如果你希望改變對象,那就用NSMutableString類型。NSString的不可變性使得空字符串對象一旦創建,就不能改變,永遠是空字符串,而所有的空字符串都是一樣的。所以出於效率的考量,Objective-C編譯器在底層就讓所有創建的空字符串指向內存中的同一個空字符串。並且這個空字符串對象是不可release掉的,因此它的引用計數值就爲UINT_MAX,表示這個對象是不可release的,那你可能會問,我如果release UINT_MAX次,是不是就釋放掉了,不是的,實際上你向這個對象發送release消息是沒有任何效果的。
 我們換非空字符串試試,輸入下面的代碼:
  1. NSString *nonEmptyStr1 = @"Hello"; 
  2. NSString *nonEmptyStr2 = [[NSString alloc] initWithString:@"Hello"]; 
  3. NSString *nonEmptyStr3 = [[NSString alloc] initWithFormat:@"%@", @"Hello"]; 
  4. NSLog(@"nonEmptyStr1 address: %p", nonEmptyStr1); 
  5. NSLog(@"nonEmptyStr2 address: %p", nonEmptyStr2); 
  6. NSLog(@"nonEmptyStr3 address: %p", nonEmptyStr3); 
  7. NSLog(@"nonEmptyStr1 retainCount: %u", nonEmptyStr1.retainCount); 
  8. NSLog(@"nonEmptyStr2 retainCount: %u", nonEmptyStr2.retainCount); 
  9. NSLog(@"nonEmptyStr3 retainCount: %u", nonEmptyStr3.retainCount); 
下面是在機器上運行結果:

       

前兩個指針仍然是指向同一個對象,原因上面已經解釋了。但是第三個不一樣,你可以從NSString的不可變性和對象的初始化方式的不同出發想想原因,相信你很快就可以得出結論的。
在Objective-C中不唯NSString是不可變對象,還有NSArray和NSDictionary。同樣你可以試試下面的代碼:
 
  1. NSArray *emptyArray1 = [[NSArray alloc] init]; 
  2. NSArray *emptyArray2 = [[NSArray alloc] init]; 
  3. NSArray *emptyArray3 = [[NSArray alloc] initWithArray:emptyArray1]; 
  4. NSLog(@"emptyArray1 address: %p", emptyArray1); 
  5. NSLog(@"emptyArray2 address: %p", emptyArray2); 
  6. NSLog(@"emptyArray3 address: %p", emptyArray3);     
  7. NSLog(@"emptyArray1 retainCount: %d", emptyArray1.retainCount); 
  8. NSLog(@"emptyArray2 retainCount: %d", emptyArray2.retainCount); 
  9. NSLog(@"emptyArray3 retainCount: %d", emptyArray3.retainCount); 
  10.  
  11. NSArray *nonEmptyArray1 = [[NSArray alloc] initWithObjects:@"1", @"2", nil]; 
  12. NSArray *nonEmptyArray2 = [[NSArray alloc] initWithObjects:@"1", @"2", nil]; 
  13. NSLog(@"nonEmptyArray1 address: %p", nonEmptyArray1); 
  14. NSLog(@"nonEmptyArray2 address: %p", nonEmptyArray2); 
  15. NSLog(@"nonEmptyArray1 retainCount: %d", nonEmptyArray1.retainCount); 
  16. NSLog(@"nonEmptyArray2 retainCount: %d", nonEmptyArray2.retainCount); 
  17.  
  18. NSDictionary *emptyDict1 = [[NSDictionary alloc] init]; 
  19. NSDictionary *emptyDict2 = [[NSDictionary alloc] init]; 
  20. NSLog(@"emptyDict1 address: %p", emptyDict1); 
  21. NSLog(@"emptyDict2 address: %p", emptyDict2); 
  22. NSLog(@"emptyDict1 retainCount: %d", emptyDict1.retainCount); 
  23. NSLog(@"emptyDict2 retainCount: %d", emptyDict2.retainCount); 
通過運行結果來進一步體會一下Objective-C編譯器底層的工作機理。
按照上面提到過的三條內存管理規則,你new、init、copy得到一個對象,你就必須負責release掉它,但實際上前面提到過的這些語句是個例外:
 
  1. NSString *s1 = [NSString new]; 
  2. NSString *s2 = [NSString alloc] initWithString:@"Hello"]; 
  3. NSArray *a = [NSArray alloc] init];  
  4. NSDictionary *dict = [NSDictionary alloc] init]; 

就算你不調用release或autorelease方法釋放也不會造成內存泄漏,你可以用Instruments檢測一下看看是否有內存泄漏。但是雖然如此,我仍強烈建議你按照內存管理三規則來處理。一致的規則不容易讓人迷惑,尤其是對閱讀你代碼的人。
 
本教程的工程文件:MemoryTest.zip
 
本文作者:安海林,軟件工程師,諾基亞北京研究院。他的格言是:學問深時意氣平!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章