在上一篇概略的提到了Objective-C裡的記憶體管理,其實Objective-C其實還滿多這種眉眉角角的地方要注意的,接下來我們來看看關於Autorelease Pool的東西。這篇文章會比較長,而且會比較悶又囉嗦,還請多多擔待。
在開始之前要先看一下這一行程式碼:
1
| |
如果用地球的語言來說,就是「建立一個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 | |
輸出結果:
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第三堂課的其中一張投影片來說明可能會比較清楚:

這個圖示就是app運作的生命週期,前半段的程式啟動、載入..等等的動作反正就是那麼一回事,重點在於wait for event跟handle event的這個迴圈(run loop),如果沒意外,這個迴圈會一直執行下去,直到可能有人打電話來中斷了你的app,或是你按了home按鈕離開這個app為止。從圖例裡可以看到而autorelease pool在每個run loop都會進行釋放。
那要怎麼樣把物件設定為autorelease? 兩個方法:
1.明確的呼叫autorelease方法
1
| |
2.上一篇有提到「當你用alloc、new或是copy開頭的方法建立一個物件的話,程式就會向系統要一塊記憶體來放這顆物件,而這顆物件就算在你頭上」,其實除了這些以外的方法來建立物件件的話,它回傳的就是個autorelease物件了。舉例來說:
1 2 3 4 | |
如果是這樣:
1
| |
這樣不需要再對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物件的方法,看起來好像沒問題,而且在其它語言也常這樣寫。但注意到在這邊這個區域變數the_name是用alloc產生的,但都沒地方把它release掉,所以可能會出問題。好吧,既然這樣那就把它release掉,所以改寫成:
1 2 3 4 5 6 | |
這樣不行,因為在return之前就release掉了。那把release動作放到return後面?
1 2 3 4 5 6 | |
這當然也不行,在return之後的動作是不會執行的。這時候,autorelease就派上用場了:
1 2 3 4 5 | |
這樣這個區域變數因為被標記成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 | |
我在這裡寫了一組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 | |
記得一開始有提到指標的東西,在這裡當把s1設定給title的時候,其實是s1跟title是指向同一個地方,所以當修改s1,title也會跟著改,這不一定是你想要的結果。更麻煩的是,當你送release訊息給s1之後,title也跟著指不到地方。所以setter的地方應該改寫成:
1 2 3 4 5 6 7 8 | |
為什麼這裡要先做判斷是否相同? 因為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 | |
這邊就不做判斷了,直接把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裡..

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