高見龍

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細節 :)

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

建議閱讀:

retainCount of NSNumber

在Objective-C裡有一種記憶體管理機制叫做Reference Counting,是說當某個物件生成並初始化之後,它的retain count會設定成1。執行該物件的”retain”方法會讓該物件的retain count加1,而release方法會讓retain count減1。當該物件的retain count變成0的時候,這個物件自動會呼叫dealloc方法,然後把記憶體還回來,Objective-C的書上或是官方手冊裡差不多都是這樣教的。

所以,下面的程式碼,2個物件的retain count預期應該會是1:

1
2
3
4
5
NSNumber *n1 = [[NSNumber alloc] initWithInt: 10];
NSNumber *n2 = [[NSNumber alloc] initWithInt: 100];

NSLog(@"retain count of n1 is %i", [n1 retainCount]);
NSLog(@"retain count of n2 is %i", [n2 retainCount]);

輸出結果是:

retain count of n1 is 2
retain count of n2 is 1

n2的retain count是1沒問題,但n1的卻是2,這結果跟我想像的不太一樣,這令我這個初學者很困惑..我知道retainCount不應該被拿來做為流程裡的邏輯判斷的依據,不過我很好奇為什麼會有這樣的差異?

翻了一下網路上的討論,原來是說因為某些數字太常被用到,為了做一些最佳化,所以在系統裡直接就預先產生了一份,再試了一下以下的程式碼:

1
2
3
4
5
6
7
8
9
10
11
NSNumber *n1 = [[NSNumber alloc] initWithInt: 10];
NSLog(@"retain count of n1 is %i", [n1 retainCount]);

NSNumber *n2 = [[NSNumber alloc] initWithInt: 100];
NSLog(@"retain count of n2 is %i", [n2 retainCount]);

NSNumber *n3 = [[NSNumber alloc] initWithInt: 100];
NSLog(@"retain count of n3 is %i", [n3 retainCount]);

NSNumber *n4 = [[NSNumber alloc] initWithInt: 10];
NSLog(@"retain count of n4 is %i", [n4 retainCount]);

結果是:

retain count of n1 is 2
retain count of n2 is 1
retain count of n3 is 1
retain count of n4 is 3

又試著把他們的位址印出來:

1
2
3
4
5
// address of nums
NSLog(@"address of n1 is %p", n1);
NSLog(@"address of n2 is %p", n2);
NSLog(@"address of n3 is %p", n3);
NSLog(@"address of n4 is %p", n4);

結果是:

address of n1 is 0x100108e20
address of n2 is 0x10010cb30
address of n3 is 0x10010cb50
address of n4 is 0x100108e20

這樣似乎就能解釋retain count跟預期不太一樣的原因了,可以看到n1跟n4是指向同一塊記憶體位置,而n2跟n3是不同的位置,retain count都是1,表示n2跟n3在生成的時候是建立一個新的數字,而n1跟n4則是指向一個像是”預先產生而且共享”的數字,所以n1一開始的 retain count是2,n4因為指向跟n1同一個地方,所以retain count變3。而這個最佳化(預先產生)的範圍,似乎是從-1 ~ 12之間。

果然還有很多要學的 :)

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

Zenburn Color Schema for FlashBuilder

我的工作需要長時間看著電腦螢幕,除了用腦袋跟手指頭敲鍵盤之外,眼睛也容易疲勞,低對比(low-contrast)的Zenburn配色一直都是我喜歡的配色方式。近期因為使用Flash Builder在趕案子,雖然說也可以進到設定的地方自己手動一個一個的調顏色,但就是調不出我想要的那個樣子,不過網路上總是有許多強大的善心人士,有人把zenburn的color schema調出來,並做成設定檔可以讓大家來匯入。

安裝方法:

1. 記得千萬一定要先備份你自己的設定檔

因為下載來的設定檔,跟你自己原來工作的環境可能是不同的,包括快速鍵、workspace…等等的設定,我就是忘了備份就直接匯入別人的設定檔,結果搞得原來的設定都跑掉了,後來去time machine裡把舊的設定檔挖出來再覆蓋回去的..

匯出設定檔:

選擇File -> Export -> Other,點開General,裡面有個Preferences的選項,接著下一步:

image

你可以選擇Export All,或是只匯出你想要的設定即可:

image

再選擇一下匯出的位置,按下Finish就行了。

2. 下載zenburn for flash builder的設定檔

http://github.com/mnem/fb-zenburnish,在這個頁面底下有說明安裝方法

下載解壓縮後,裡面有個fb-zenburnish.epf跟README檔案(記得要看一下內容)。

如同剛剛匯出的步驟,只是這次是反過來操作。選擇File -> Import -> Other,在General裡找到Preferences選項,選擇你要匯入的epf檔案,沒問題的話按下Finish就完成了。

但 對我來說,因為我想要保留我自己習慣的設定,我只要zenburn的color schema設定就好,所以我的作法是用文字編輯器開啟剛下載的fb-zenburnish.epf後,把內容貼到我自己剛才匯出的設定檔的最後面,最後 再把修改過的設定檔匯回來,這樣我就可以保有我自己的設定以及zenburn的color schema了。

成果照:

image

對於長時間需要用眼力的人來說,不妨試試看效果,也許會比較輕鬆一些些(未經人體實驗證實,但至少對我來說是有用的)。不過顏色的喜好是很主觀的,也許我覺得喜歡顏色配置的對其它人來說反而可能比預設的更糟或更傷眼..

另外,我也手癢的把Xcode改成zenburn配色了:

  1. 下載:http://mac.softpedia.com/get/Graphics/Xcode-Zenburn-Theme.shtml
  2. 把下載的xccolortheme檔案複製到 ~/Library/Application Support/Xcode/Color Themes/ 底下
  3. 開啟Xcode,進入Preferences的Fonts & Colors項目,在下拉選單就可以看到zenburn的選項了

成果照:

image