一、前言
對于大多數從C++或者JAVA轉過來學習Object-C(以下簡稱OC)的人來說,OC這門語言看起來非常奇怪,用起來也有點麻煩。
OC沒有像JAVA一樣的垃圾回收機制,也就是說,OC程式設計需要程式員手動去管理記憶體。這就是為什麼它煩的原因,蘋果卻一直推崇開發者在有限硬體資源内寫出最優化的代碼,使用CPU最少,占用記憶體最小。
二、基本原理
對象的建立:
OC在建立對象時,不會直接傳回該對象,而是傳回一個指向對象的指針,是以出來基本類型以外,我們在OC中基本上都在使用指針。
ClassA *a = [[ClassA
alloc] init];
在[ClassA
alloc]的時候,已經發送消息通知系統給ClassA的對象配置設定記憶體空間,并且傳回了指向未初始化的對象的一個指針。
未初始化的ClassA對象接手到init消息,init傳回指向已初始化後的ClassA對象的一個指針,然後将其指派給變量a。
在建立并使用完一個對象的時候,使用者需要手動地去釋放該對象。
[a dealloc];
如果指針a和b同時指向堆中同一塊記憶體位址
ClassA *b = a;
當執行到第三行的時候,指針b就成了無頭指針。這是一個在C++中也是常見的錯誤,我們需要避免這類錯誤,因為無頭指針是危險的。
引用計數:
OC在記憶體管理上采用了引用計數(retain
count),在對象内部儲存一個數字,用來表示被引用的次數。init、new和copy都會讓retain
count加1。當銷毀對象的時候,系統不會直接調用dealloc方法,而是先調用release,讓retain count 減1,當retain
count等于0的時候,系統才會調用dealloc方法來銷毀對象。
在指針指派的時候,retain count
是不會自動增加的,為了避免上面所說的錯誤,我們需要在指派的時候手動retain一次,讓retain count 增加1。
alloc] init]; // retain count = 1
[b retain]; // retain count
= 2
這樣在執行到第四行的時候,對象的retain count隻是減了1,并沒有被銷毀,指針b仍然有效。
記憶體洩露:
就如上面列子所示,當生成ClassA對象時,指針a擁有對該對象的通路權。如果失去了對一個對象的通路權,而又沒有将retain
count減至0,就會造成記憶體洩露。也就是說,配置設定出去的記憶體無法回收。
a = nil;
三、Autorelease Pool
為了友善程式員管理記憶體,蘋果在OC中引入了自動釋放池(Autorelease
Pool)。在遵守一些規則的情況下,可以自動釋放對象。但即使有這麼一個工具,OC的記憶體仍需要程式員時刻關注(這個自動釋放池跟JAVA的垃圾回收機制不是一回事,或者說,騎馬都追不上JAVA的機制,可能連塵都吃不到)。
ClassA *a = [[[ClassA
alloc] init] autorelease];
//retain count = 1,但無需release
Autorelease Pool 的原理:
autorelease pool
全名叫做NSAutoreleasePool,是OC中的一個類。autorelease pool并不是天生就有的,你需要手動的去建立它
NSAutoreleasePool *pool =
[[NSAutoreleasePool alloc] init];
一般地,在建立一個iphone 項目的時候,xcode會自動地為你建立一個autorelease pool,這個pool就寫在Main函數裡面。
在NSAutoreleasePool中包含了一個
可變數組,用來存儲被聲明為autorelease的對象。當NSAutoreleasePool自身被銷毀的時候,它會周遊這個數組,release數組中的每一個成員(注意,這裡隻是release,并沒有直接銷毀對象)。若成員的retain
count 大于1,那麼對象沒有被銷毀,造成記憶體洩露。
預設的NSAutoreleasePool
隻有一個,你可以在你的程式中建立NSAutoreleasePool,被标記為autorelease的對象會跟最近的NSAutoreleasePool
比對。 NSAutoreleasePool *pool =
//Create some objects
//do something…
[pool release];
你也可以嵌套使用NSAutoreleasePool ,就像你嵌套使用for一樣。
即使NSAutoreleasePool
看起來沒有手動release那麼繁瑣,但是使用NSAutoreleasePool
來管理記憶體的方法還是不推薦的。因為在一個NSAutoreleasePool
裡面,如果有大量對象被标記為autorelease,在程式運作的時候,記憶體會劇增,直到NSAutoreleasePool
被銷毀的時候才會釋放。如果其中的對象足夠的多,在運作過程中你可能會收到系統的低記憶體警告,或者直接crash。
Autorelease Pool 擴充:
如果你極具好奇心,把Main函數中的NSAutoreleasePool
代碼删除掉,然後再自己的代碼中把對象聲明為autorelease,你會
發現系統并不會給你發出錯誤資訊或者警告。用記憶體檢測工具去檢測記憶體的話,你可能會驚奇的發現你的對象仍然被銷毀了。
其實在新生成一個Run
Loop的時候,系統會自動的建立一個NSAutoreleasePool ,這個NSAutoreleasePool 無法被删除。
在做記憶體測試的時候,請不要用NSString。OC對字元串作了特殊處理
NSString *str =[ [NSString
alloc] stringWithString:@”123”];
在輸出str的retain count 的時候,你會發現retain count 大于1。
四、手動管理記憶體
使用alloc、new、copy建立一個對象,該對象的retain count
都等于1,需要用release來釋放該對象。誰建立,誰去釋放。在這3鐘方法以外的方法建立的對象,都被系統預設的聲明為autorelease。
ClassA *b = a;
[b retain];
//do smoething
[b release];
b = nil;
把一個指針指派給另外一個指針的時候,a 指針所指向的對象的引用次數并沒有增加,也就是說,對象的retain
count依然等于1。隻有在retain了之後,retain count 才會加1。那麼,如果這時候執行[a
release],隻是a指針放棄了對對象的通路權,對象的retain count
減1,對象沒有被銷毀。隻有當b也執行了release方法之後,才會将對象銷毀掉。是以,誰retain了,誰就要release。
在對象被銷毀之後,指針依然是存在的。是以在release了之後,最好把指針賦為空,防止無頭指針的出現。順便一說,release一個空指針是合法的,但是不會發生任何事情。
如果你在一個函數中建立并傳回一個對象,那麼你需要把這個對象聲明為autorelease
(ClassA *)Function()
{
ClassA *a =
[[[ClassA alloc] init] autorelease];
return a;
}
不這樣做的話,會造成記憶體洩露。
五、屬性與記憶體管理
蘋果一直沒有強調的一點是,關于屬性中的retain。事實上,屬性中帶有retain的,在指派的時候可能已經在合成的setter中retain了一次,是以,這裡也需要release。
@property實際上是getter和setter,@synthesize是合成這2個方法。為什麼在聲明了屬性之後可以用“.”來直接調用成員變量呢?那是因為聲明屬性以後系統根據你給的屬性合成了一個set方法和一個get方法。使用“.”與屬性并沒有直接關聯,如果你不嫌麻煩,在你的程式裡面多寫一個set和get方法,你也可以使用“.”來調用變量。
@property(),如果你裡面什麼都不寫,那麼系統會預設的把你的屬性設定為:
@property(atomic, assign)…..
關于nonatomic:
這個屬性沒有對應的atomic關鍵字,即使我上面是這麼寫,但atomic隻是在你沒有聲明這個特性的時候系統預設,你無法主動去聲明這一特性。
如果你的程式隻有一個主線程,或者你确定你的程式不會在2個或者以上線程運作的時候通路同一個變量,那麼你可以聲明為nonatomic。指定nonatomic特性,編譯器合成通路器的時候不會去考慮線程安全問題。如果你的多個線程在同一時間會通路到這個變量的話,可以将特性聲明為atomic(通過省略關鍵字nonatomic)。在這種特性的狀态下,編輯器在合成通路器的時候就會在通路器裡面加一個鎖(@synchronized),在同一時間隻能有一個線程通路該變量。
但是使用鎖是需要付出代價的,一個聲明為atomic的屬性,在設定和擷取這個變量的時候都要比聲明為nonatomic的慢。是以如果你不打算編寫多線程代碼,最好把變量的屬性特性聲明為nonatomic。
關于assign、retain和copy:
assign是系統預設的屬性特性,它幾乎适用于OC的所有變量類型。對于非對象類型的變量,assign是唯一可選的
特性。但是如果你在引用計數下給一個對象類型的變量聲明為assign,那麼你會在編譯的時候收到一條來自編譯器的警告。因為assign對于在引用計數下的對象特性,隻建立了一個弱引用(也就是平時說的淺複制)。這樣使用變量會很危險。當你release了前一個對象的時候,被指派的對象指針就成了無頭指針了。是以在為對象類型的變量聲明屬性的時候,盡量少(或者不要)使用assign。
關于assign合成的setter,看起來是這樣的:
-(void)setObjA:(ClassA *)a
objA
= a;
在深入retain之前,先把聲明為retain特性的setter寫出來:
{
If(objA !=
a)
[objA release];
objA = a;
[objA retain]; //對象的retain count 加1
}
明顯的,在retain的setter中,變量retain了一次,那麼,即使你在程式中
self.objA = a;
隻寫了這麼一句,objA仍然需要release,才能保證對象的retain count 是正确的。但是如果你的代碼
objA = a;
隻寫了這麼一句,那麼這裡隻是進行了一次淺複制,對象的retain count 并沒有增加,是以這樣寫的話,你不需要在後面release objA。
這2句話的差別是,第一句使用了編譯器生成的setter來設定objA的值,而第二句隻是一個簡單的指針指派。
copy的setter看起來是這樣的:
ClassA * temp = objA;
objA = [a copyWithZone:nil];
[temp release];
複制必須通過實作copyWithZone:這個方法,因次copy這個特性隻适用于擁有這個方法的類型,也就是說,必須這個類支援複制。複制是把原來的對象release掉,然後讓指針指向一個新的對象的副本。是以即使在setter裡面release了原來的對象,你仍然需要在後面release新指向的對象(副本)。
六、尾聲
IOS開發現在唯一能用的記憶體管理方式就是引用計數,無論你喜歡還是不喜歡。在一個記憶體緊缺的機器上,你編寫程式的時候也隻能步步為營,盡可能的讓你的程式騰出記憶體空間,并保證系統不會給你一個警告。即使蘋果在Mac
OS X 雪豹(v10.5)系統裡面添加了另外一種記憶體管理方式(垃圾收集),但目前不适用于IOS。