高見龍

iOS app/Ruby/Rails Developer & Instructor, 喜愛非主流的新玩具 :)

Objective-C記憶體管理之Autorelease

上一篇概略的提到了Objective-C裡的記憶體管理,其實Objective-C其實還滿多這種眉眉角角的地方要注意的,接下來我們來看看關於Autorelease Pool的東西。這篇文章會比較長,而且會比較悶又囉嗦,還請多多擔待。

在開始之前要先看一下這一行程式碼:

1
NSMutableString *s1 = [NSMutableString stringWithString:@"hello eddie"];

如果用地球的語言來說,就是「建立一個NSMutableString型態的s1指標變數,這個指標變數指向某個內容為”hello eddie”的NSMutableString物件」,在s1前面的那個星號*表示這個s1是一個指標(pointer)變數,不過這裡不會對指標有太多的說明(因為我跟指標也不熟),有興趣可再找找C/C++的相關書籍來研究。在Objective-C裡的物件變數都是指標,所以來看看底下這個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#import <Foundation/Foundation.h>
int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

  // 建立2個字串變數
  NSMutableString *s1 = [NSMutableString stringWithString:@"hello eddie"];
  NSMutableString *s2 = s1;

  // 印出這兩個字串的位置
  NSLog(@"location of s1 is %p", s1);
  NSLog(@"location of s2 is %p", s2);

  // 印出兩個字串的內容
  NSLog(@"s1 is %@", s1);
  NSLog(@"s2 is %@", s2);

  // 改變了s1的內容
  [s1 setString:@"eddie"];

  // 看看這兩個字串的內容是否改變
  NSLog(@"s1 is %@", s1);
  NSLog(@"s2 is %@", s2);

  [pool drain];
  return 0;
}

輸出結果:

location of s1 is 0x10010cad0
location of s2 is 0x10010cad0
s1 is hello eddie
s2 is hello eddie
s1 is eddie
s2 is eddie

因為s1跟s2其實是指向同一個地方,所以印出來的記憶體位置也是相同的。一但修改了s1的內容,指向同一個地方的s2也會跟著改變。為什麼要特別提這個? 待會後面會用到。

回到主題,在上一篇文章有提到東西用完要release掉,把記憶體還給系統,這個autorelease pool照字面看起來像是會自動釋放資源的東西,是沒錯,但其實它並不是Garbage Collection。簡單的說,想像你有個游泳池(pool),裡裝著一堆被標記為autorelease的客人,當這個池子裡的水要放掉的時候,它會自動對這個池子裡的所有客人發送release訊息叫他們滾蛋,上一篇也提到,當這些客人的retain count降到0的時候,dealloc方法會自動被呼叫,然後把自己清掉並歸還記憶體。

有點複雜嗎? 我直接借用Stanford大學CS193P第三堂課的其中一張投影片來說明可能會比較清楚:

image

這個圖示就是app運作的生命週期,前半段的程式啟動、載入..等等的動作反正就是那麼一回事,重點在於wait for event跟handle event的這個迴圈(run loop),如果沒意外,這個迴圈會一直執行下去,直到可能有人打電話來中斷了你的app,或是你按了home按鈕離開這個app為止。從圖例裡可以看到而autorelease pool在每個run loop都會進行釋放。

那要怎麼樣把物件設定為autorelease? 兩個方法:

1.明確的呼叫autorelease方法

1
NSNumber *n1 = [[[NSNumber alloc] initWithInt:100] autorelease];

2.上一篇有提到「當你用alloc、new或是copy開頭的方法建立一個物件的話,程式就會向系統要一塊記憶體來放這顆物件,而這顆物件就算在你頭上」,其實除了這些以外的方法來建立物件件的話,它回傳的就是個autorelease物件了。舉例來說:

1
2
3
4
// 你用alloc建立的,用完就要把它release掉
NSNumber *n1 = [[NSNumber alloc] initWithInt:100];
// 用完之後release
[n1 release];

如果是這樣:

1
NSNumber *n1 = [NSNumber numberWithInt:100];

這樣不需要再對n1呼叫autorelease方法,這個n1就是autorelease了。

要特別注意的是,autorelease跟release不同,release會馬上在執行後把retain count減1,而autorelease則是待會才會減1。另外,在上一篇最後提到的記憶體管理原則:「You only release or autorelease objects you own」,一但你把物件設定為autorelease之後,這個東西基本上就不算是你管的,你之後就不需要也不應該再去對它做release的動作了。

或許你會覺得用一般的release就好了嗎? 為什麼特別需要用到這個autorelease? 其實有些地方還是真的非它不可,我們來看看底下這個例子:

1
2
3
4
5
-(NSString *) getBookName
{
  NSString *the_name = [[NSString alloc] initWithString:@"This is a book"];
  return the_name;
}

這裡我們寫了一個會回傳NSString物件的方法,看起來好像沒問題,而且在其它語言也常這樣寫。但注意到在這邊這個區域變數the_name是用alloc產生的,但都沒地方把它release掉,所以可能會出問題。好吧,即然這樣那就把它release掉,所以改寫成:

1
2
3
4
5
6
-(NSString *) getBookName
{
  NSString *the_name = [[NSString alloc] initWithString:@"This is a book"];
  [the_name release];
  return the_name;
}

這樣不行,因為在return之前就release掉了。那把release動作放到return後面?

1
2
3
4
5
6
-(NSString *) getBookName
{
  NSString *the_name = [[NSString alloc] initWithString:@"This is a book"];
  return the_name;
  [the_name release];
}

這當然也不行,在return之後的動作是不會執行的。這時候,autorelease就派上用場了:

1
2
3
4
5
-(NSString *) getBookName
{
  NSString *the_name = [[NSString alloc] initWithString:@"This is a book"];
  return [the_name autorelease];
}

這樣這個區域變數因為被標記成autorelease,它在待會,也就是run loop結束的時候會自動被釋放掉了。

再舉個例子,在Objective-C裡,物件的instance variable預設是設定為protected的,如果沒有getter/setter的話是沒辦法取得取得或設定該物件的屬性(如果你是用@property/@synthesis的做法就不用這麼麻煩了)。所以,舉個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface Book : NSObject
{
  NSString *title;
}

-(void) setTitle: (NSString *)book_title;
-(NSString *)title;
@end

@implementation Book
-(void) setTitle: (NSString *)book_title
{
  title = book_title;
}

-(NSString *)title
{
  return title;
}
@end

我在這裡寫了一組getter跟setter,分別可以設定及讀取Book這個類別的title屬性,看起來是很直覺的getter/setter寫法,在其它的程式語言中也常這樣寫。其實在有GC的環境,這樣寫是沒問題的[註1],但在不支援GC的環境上,例如iphone,這樣的寫法可能會出問題,直接來看個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

  // 建立物件
  Book *book = [[Book alloc] init];
  NSMutableString *s1 = [NSMutableString stringWithString:@"hello eddie"];

  // 把字串s1透過setter設定給title屬性
  [book setTitle:s1];

  // 輸出結果:the title of the book is 'hello eddie'
  NSLog(@"the title of the book is '%@'", [book title]);

  // 修改s1的內容
  [s1 setString:@"not eddie"];

  // 輸出結果:the title of the book is 'not eddie'
  NSLog(@"the title of the book is '%@'", [book title]);

  // 把s1釋放掉
  [s1 release];

  // 這一行就爆炸了..
  NSLog(@"the title of the book is '%@'", [book title]);

  [book release];
  [pool drain];
  return 0;
}

記得一開始有提到指標的東西,在這裡當把s1設定給title的時候,其實是s1跟title是指向同一個地方,所以當修改s1,title也會跟著改,這不一定是你想要的結果。更麻煩的是,當你送release訊息給s1之後,title也跟著指不到地方。所以setter的地方應該改寫成:

1
2
3
4
5
6
7
8
-(void) setTitle: (NSString *)book_title
{
  if (title != book_title)
  {
      [title release];
      title = [book_title retain]; // 或是copy,端看用途而定
  }
}

為什麼這裡要先做判斷是否相同? 因為title跟傳進來的book_title有可能會指向同一個物件,如果直接先把title release掉,會導致book_title一起跟著無法存取。如果title跟傳進來的book_title的記憶體位置不同的話,那就把原來的title放掉,並從把傳進來的book_title做retain(或copy),確保retain count增加,即使像剛剛那個情況把傳進來的東西release掉,因為retain count不會降到0,所以就還會留著。這裡也可以使用autorelease來處理:

1
2
3
4
5
-(void) setTitle: (NSString *)book_title
{
  [title autorelease];
  title = [book_title retain]; // or copy
}

這邊就不做判斷了,直接把title設定成autorelease,然後待會交給autorelease pool去放掉就好。

這樣看來,感覺autorelease好像很方便,何不乾脆把全部都交給autorelease pool處理好了? 的確是比較方便沒錯,但如果可以的話,請儘量用手動的release而不要用自動的autorelease。一來程式設計師能在資源用完之後馬上主動還回去本就是個好習慣,二來autorelease是在每個run loop才釋放一次,雖然每個run loop可能是零點零零幾秒而已,但也有可能在一個run loop裡的某個迴圈裡一下子產生太多的物件,還沒跑到pool釋放的地方就爆炸了(通常遇到這種情況會在產生大量物件的地方放個巢狀的pool在裡面,讓內層的pool提早釋放,而不是等到最外層的run loop才釋放)。

希望這篇文章對一起學習的朋友會有幫助,若內容有誤也請不吝指正。

建議閱讀:

註1:GC支援功能是可以手動打開,不過要記得不是每個環境都有GC可以用的,在Project Settings裡..

image

找到Objective-C Garbage Collection的選項,設定成Supported:

image

Comments