高見龍

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

Objective-C的記憶體管理

Objective-C的記憶體管理是個重要的主題,把最近啃書的一些心得寫下來讓自己來加深自己的印象,也順便把觀念整理一下。

之前工作上寫習慣PHP/Python/Ruby/AS3這種比較接近人類腦袋的程式語言,很少有機會去碰到pointer(指標)或是記憶體管理的東西。為什麼變數宣告就宣告,為什麼前面要加個星號(*)? 又有的又不用加? 最近在看Objective-C的書就會覺得很不習慣(不過其實看久了也會慢慢習慣就是了)。

在Objective-C裡的記憶體管理主要有兩種,一個是比較常聽到的Garbage Collection(以下縮寫為GC),另一個則是Reference Counting(以下縮寫為RC)。以車子來舉例的話,你可以先暫時把這兩種記憶體管理方式想像成GC是自排車,RC是手排車,一個會自動幫忙回收沒要用的記憶體,一個則是自己得手動釋放用過不要再用的記憶體。

RC的運作機制是當某個物件生成並初始化之後,它的初始retain count會設定成1(其實不一定,也是有例外)。執行該物件的”retain”方法會讓該物件的retain count加1,而release方法會讓retain count減1。當該物件的retain count降到0的時候,這個物件自動會呼叫dealloc方法把自己解決掉,然後把佔用的記憶體還回來。

也許你會覺得,都什麼年代了為什麼還得程式設計師自己用手動的方式來回收記憶體? 換個角度想,程式設計師為自己寫的程式負責也是件好事,另外,也是最主要的原因就是有些環境就是根本不支援GC機制,所以只好用RC來處理。我相信想學Objective-C很多人都是為了想要開發iPhone App而來的,而iPhone正是那個不支援GC環境的其中之一。

又或許你會覺得這樣一顆小物件是能佔多少記憶體。這種東西積沙成塔的,你借了記憶體來用卻沒還回去,久了可能就會造成”漏水”(memory leaking)的情況。為了避免App在執行的過程中莫名奇妙的地方當掉,只好乖乖的來了解一下關於記憶體管理的機制。

Apple官方的開發手冊裡,光是記憶體管理的部份就有五十幾頁的PDF,建議大家抓回來看一下。官方手冊裡開頭提到關於記憶體管理的大原則:

You only release or autorelease objects you own.

什麼是你擁用的物件? 當你用allocnew或是copy開頭的方法建立一個物件的話,程式就會向系統要一塊記憶體來放這顆物件,而這顆物件就算在你頭上。另外當你用對某個物件使用retain方法之後,那顆物件也算是你要負責的;一個物件可以同時有好幾個主人,而當那個物件你不要用的時候,則使用release或是autorelease方法來把這個擁有的關係給斷絕掉,準備把物件清掉並把佔用的記憶體還給系統。直接來看段範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
  NSNumber *n = [[NSNumber alloc] initWithInt:100];

  NSLog(@"the retain count is %d", [n retainCount]);

  [n retain];

  NSLog(@"the retain count is %d", [n retainCount]);

  [n release];

  NSLog(@"the retain count is %d", [n retainCount]);

  [n release];

  [pool drain];
  return 0;
}

輸出結果:

the retain count is 1
the retain count is 2
the retain count is 1

除了retain/release之外,如果被collections,例如array、dictionary或set等等給拉進去的話,它的retain count也會加1;相對的,從Collection裡拿出來的話,它的retain count會減1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
  NSMutableArray *array = [[NSMutableArray alloc] init];
  NSNumber *n = [[NSNumber alloc] initWithInt:100];

  NSLog(@"the retain count of n is %d", [n retainCount]);

  [array addObject:n];

  NSLog(@"the retain count of n is %d", [n retainCount]);

  [n release];

  NSLog(@"the retain count of n is %d", [n retainCount]);

  [array release];

  [pool drain];
  return 0;
}

輸出結果:

the retain count of n is 1
the retain count of n is 2
the retain count of n is 1

在這個例子裡可以看到,當使用addObject方法把n放進array之後,它的retain count會加1;當使用removeObjectAtIndex把物件從array移出來的時候,它的retain count會減1。最後,當array本身收到release的時候,它會對目前全部的內容物發送release訊息。所以在上面的例子來說,如果在[array release]之後再想存取n變數,就會出現錯誤訊息。

記得,你retain了一個物件,確定沒要再用之後就release掉。retain跟release的次數通常是成對的,你手動retain了幾次,到時候就得手動release幾次。

有一些常見可能會發生問題的寫法:

1
2
3
4
5
- (void)reset
{
  NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
  [self setCount:zero];
}

這裡用alloc建立物件,卻沒有對等的release,可能造成memory leak。

1
2
3
4
5
6
- (void)reset
{
  NSNumber *zero = [NSNumber numberWithInteger:0];
  [self setCount:zero];
  [zero release];
}

在這裡numberWithIngeter產生的是一個autorelease物件,所以這並不屬於你擁有的,當你對它執行release它可能就會因為retain count變成0而被清掉,在autorelease pool裡因為會再對所有標記為autorelease的物件再送一次release訊息,這時候就會出錯了。請記憶體釋放的原則:「You only release or autorelease objects you own.」,如果物件不屬於你,就不要隨便對它執行release方法

關於Autorelease pool,你可能會在一些剛建立好的專案裡看到它幫寫好幾行程式碼了,autorelease pool並不是真正的GC機制,它比較像是GC的替代品。簡單的說,當你對物件執行autorelease方法時,就是把該物件標記成”待會再釋放”的物件,在每個run loop或是pool drain的時候就會對這些有標記的所有物件發送release訊息。雖說是替代品,但有些地方還是非用不可。

結論:

  1. 在你跟系統要了一塊記憶體來用的當下,請先養成「我什麼時候會還回去」的習慣。
  2. retain/release通常都是成對的,手動做了N次的retain,就記得要做N次的release。
  3. 如果你要某個物件,就retain它;如果不用了,就release它,把記憶體還回去。

之後會再來說明一些關於Autorelease pool細節 :)

新手上路,若有錯誤還請不吝指教

建議閱讀:

Comments