Objective-C中的記憶體管理機制
從蘋果的官方文件來看,OC對應用程式的記憶體管理提供了2種方法。
第一種即“manual retain-release”(MRR),手動保留釋放,也可理解為手動引用計數。
第二種,“Automatic Reference Counting”(ARC),自動引用計數。但是ARC並不等同垃圾回收。在蘋果的官方文件有這樣一句話,“You are strongly encouraged to use ARC for new projects.”意思是蘋果強烈建議在專案中運用ARC機制來管理記憶體。
記憶體管理不當的話會出現以下2種問題:
1.過早釋放:在某處程式用完某塊記憶體之前,就將該記憶體還給了“堆”。(這裡的堆指的是,ios啟動應用時,會為應用保留一部分空閒的RAM,這部分空閒的RAM稱為堆。應用程式可隨意使用堆,不會影響ios的其他部分,也不會影響其他應用。)
2.記憶體洩露:不釋放已經不使用的記憶體會導致記憶體洩露,即使他從來沒有被再次使用過。記憶體洩露會導致你的應用程式的記憶體使用量日益增加,這反過來有可能會導致系統性能較差或申請記憶體被終止。
要說明的一點是不管是MRR中的“通過屬性機制簡化存取方法”(在“存”方法中涉及到了基本的記憶體管理),還是ARC。本質上都是蘋果幫助程式設計師在開發時減少了程式碼量,把原來由程式設計師要完成的工作交給編譯器去完成,從而減少軟體開發的繁瑣程度。
接下來就MRR和ARC進行詳細的說明。
在OC中所有的類均繼承於基類NSObject,那麼所有的類就都有一個類方法alloc和一個例項方法dealloc。當通過向類傳送alloc方法來建立類例項時,系統會從堆中分配出相應位元組數的記憶體(注:指標型別的例項變數大小是4個位元組,這是儲存堆中的物件地址所需的記憶體空間)。例如:UIView *view = [[UIView alloc] init]; alloc會返回一個指標物件,指向新分配的記憶體。分配記憶體後,在類完成其“功能”後,還要將記憶體還給堆。但是不可以直接向物件傳送dealloc方法,即這樣寫[view dealloc]是不對的,只能由物件自己向自己傳送dealloc方法。對於dealloc,蘋果官方文件是這麼解釋的:The NSObject class also defines a method, dealloc, that is invoked automatically when an object is deallocated(NSObject定義了一個方法dealloc,當物件被釋放時自動呼叫)。那麼物件何時釋放?釋放時是否安全呢?這個在OC中是通過引用計數來解決這個問題的。
一.引用計數演算法
物件建立後,這個物件就有一個所有者。物件在其生命週期可以有不同的所有者,也可以同時有多個所有者,引用計數既是用來記錄所有者的數量。當物件沒有所有者時,即引用計數為0時,就會釋放自己。作為物件本身不需要知道所有者是誰,只需知道所有者的個數。物件通過retain計數跟蹤所有者的數量。這個是通過NSObject定義的協議與標準方法命名約定相結合的方法來實現的。引用計數其實是在進行“責任落實”:誰建立了物件(或保留了已經建立的物件),誰就是該物件的所有者。釋放物件即放棄該物件的所有權。誰有物件的所有權,誰就要負責放棄該所有權。在不能再向相應物件傳送訊息時,即不再擁有指向該物件的指標時,需要放棄該所有權。但此演算法無法回收迴圈引用的儲存物件。Cocoa目前採用的就是此種機制。(Cocoa是蘋果公司為Mac OS X所建立的原生面向物件的API,是Mac OS X上五大API之一,其它四個是Carbon、POSIX、X11和Java)
manual retain-release(MRR)
MRR可以理解為當物件建立後,會有一個所有者,即新建物件的retain計數是1.當物件得到某個所有者時,retain計數+1,當物件失去某個所有者時,呼叫release方法,retain計數-1.當物件沒有任何所有者時,retain計數為0.物件會自動呼叫dealloc,將所佔用的記憶體還給堆。用程式碼來實現就是:
- (id)retain
{
retainCount++;
return self;
}
- (void)release
{
retainCount--;
if(retainCount == 0){
[self dealloc];
}
}
複製程式碼
何為所有者?何為擁有該物件的所有權?就是當你在OC中用“alloc”, “new”, “copy”, or “mutableCopy”方法建立物件後,即是此物件的擁有者。還有就是當你在保留某一物件的值的時候,也是擁有了該物件(屬性中的set方法深刻的說明了這一點)。以nane屬性為例,它的set方法應該寫為:
- (void)setName:(NSString *)str
{
[str retain];
[name release];
name = str;
}
複製程式碼
這裡必須先保留新物件,再釋放當前物件。這是因為name和str有可能指向同一個物件。如果顛倒順序,就有可能釋放掉原本打算作為name保留的物件。在類中,當類擁有其它例項物件的時候,要在dealloc方法中將其release掉。
引用計數的規則:
1.如果用來建立物件的方法,其方法名是以alloc或new開頭的,或是包含copy和mutableCopy,那麼你已經擁有該物件的所有權。你要負責在不需要該物件的時候將其釋放。
2.如果你不擁有某個物件,但是要確保該物件繼續存在,那麼可以通過向其傳送retain訊息來獲得所有權(retain計數+1)。
3.當你擁有某個物件並且不再需要該物件的時候,要release或autorelea掉。(下面會詳細介紹autorelease)
4.只要物件還有至少一個所有者,該物件就會繼續存在下去,只有在retain計數為0時,才會收到dealloc訊息。
使用自動釋放池(autorelease)
蘋果官網是這樣解釋的:自動釋放池塊提供了一種機制,讓你可以放棄物件的所有權,但要避免它被立即釋放(例如,當您返回一個物件的類方法)的可能性。通常情況下,你並不需要建立自己的autorelease池塊。在OC中的類方法,是為“他人”建立物件,“自己”不擁有,也不使用。那類方法中物件的記憶體怎麼管理呢?這裡需要某種解決方案,能夠暫時不釋放物件,但具備釋放該物件的權利。通過向物件傳送autorelease訊息,可以將物件標記為“稍後釋放”。當物件收到autorelease後,不會馬上釋放,而是會加入一個NSAutoreleasePool例項。該NSAutoreleasePool例項會記錄所有標記為“稍後釋放”的物件。每隔一段時間,這個NSAutoreleasePool例項會被“排幹(drain)”,這時它會向其包含的所有物件傳送release訊息,然後移除這些物件。
標記為autorelease的物件有2種命運:要麼走完物件的生命週期,直到被釋放,要麼被另外一個物件保留。當某一物件保留了標記為autorelease的物件後,那麼他的retain count計數會變成2,將來的某個時候,NSAutoreleasePool例項會釋放該物件,使其retain count計數為1.何為“將來的某個時候”?ios應用在執行時,存在一個執行迴圈(run loop)。該執行迴圈等待事件(event)的發生,例如觸控事件或定時器觸發(NSTimer)等等,當事件發生時,應用會跳出執行迴圈並通過呼叫某個類方法來處理相應的事件。程式碼執行完畢後,應用將返回當前的執行迴圈。每次迴圈結束,所有標記為autorelease的物件都會收到release訊息。
Automatic Reference Counting(ARC)
ARC機制極大的減少了開發過程中常見的程式錯誤:retain跟release不匹配。ARC並不會消除對retain和release的呼叫,而是把這項原本大都屬於開發者的工作移交給了編譯器。ARC並不等同於垃圾回收。retain和release仍然會被呼叫,所以有一些開銷,在release的時候可能還會呼叫dealloc方法。這段程式碼與程式設計師手動呼叫retain和release的程式碼在執行結果上是完全一致的。垃圾回收機制是在執行時起作用的,會影響執行效率,而ARC是在編譯時插入記憶體管理程式碼,不影響執行時效率,因此記憶體回收比垃圾回收時的效率要高,能夠提升系統性能。這種編譯器可以自由地以多種方式優化記憶體管理,而讓程式設計師手動去做這些工作是不現實的。在多數情況下,使用ARC生成記憶體管理程式碼的程式比程式設計師手工新增記憶體管理程式碼的對等程式執行更快!
ARC不是垃圾回收,尤其是它不能像Snow Leopard中的垃圾回收機制那樣處理迴圈引用。因此,在ios開發中,必須要做好對強引用(strong reference)的跟蹤管理以免出現迴圈引用。屬性關係有兩種主要型別:strong和weak。相當於非ARC環境裡的retain和assign。只要存在一個強引用,物件就會一直存在,不會被銷燬。OC中一直存在迴圈引用的問題,但在實際應用中很少出現迴圈引用。對於過去那些使用assign屬性的地方,在ARC環境中要使用weak代替。大部分引用迴圈是由委託(delegate)引起的,所以應該總是把delegate屬性宣告為weak。當引用的物件被銷燬之後,weak引用會被自動設定為nil,與assign相比這是一個巨大的進步,因為assign可以指向被釋放掉的記憶體,導致程式奔潰。
二.可達性分析演算法
近現代的垃圾回收實現方法,通過定期對若干根儲存物件開始遍歷,對整個程式所擁有的儲存空間查詢與之相關的儲存物件和沒相關的儲存物件進行標記,然後將沒相關的儲存物件所佔物理空間回收。既通過一系列的GC Roots的物件作為起始點,從這些根節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。即是可回收的。此演算法可回收迴圈引用的儲存物件。(Java和C#語言採用的機制)
引申閱讀:深拷貝和淺拷貝
深拷貝:簡單說就是對指標指向的內容進行拷貝,以字串為例,就是指建立一個新的指標在一個新的地址區域建立一個字串,這個字串與原字串值相同,新的指標指向這個新建立的字串。而原字串的引用計數沒有+1
淺拷貝:既指標拷貝,例如一個指標指向一個字串,也就是說這個指標變數的值是這個字串的地址,那麼對這個指標拷貝就是又建立了一個指標變數,這個指標變數的值是這個字串的地址,也就是這個字串的引用計數+1
關於深淺拷貝看原始碼一目瞭然,以NSString和NSMutableString為例:
- (id)copyWithZone:(NSZone*)zone
{
if (NSStringClass == Nil) NSStringClass = [NSString class];
return RETAIN(self)
}
- (id)mutableCopyWithZone:(NSZone*)zone
{
return [[NSMutableString allocWithZone:zone] initWithString:self];
}
複製程式碼
看上面程式碼,當屬性設定copy時,實際呼叫的就是copyWithZone方法,而copyWithZone並沒有建立新的物件,而是使指標持有了原來的物件,即淺拷貝。而屬性設定mutableCopy時,呼叫的就是mutableCopyWithZone方法,而這個方法建立了一個新的可變字串物件,即深拷貝。