以下是關於記憶體管理的學習筆記:引用計數與ARC。 iOS5以前自動引用計數(ARC)是在MacOS X 10.7與iOS 5中引入一項新技術,用於代替之前的手工引用計數MRC(Manual Reference Counting)管理Objective-C中的對象【官方也叫MRR(Manual Ret ...
以下是關於記憶體管理的學習筆記:引用計數與ARC。
iOS5以前自動引用計數(ARC)是在MacOS X 10.7與iOS 5中引入一項新技術,用於代替之前的手工引用計數MRC(Manual Reference Counting)管理Objective-C中的對象【官方也叫MRR(Manual Retain Release)】。如今,ARC下的iOS項目幾乎把所有記憶體管理事宜都交給編譯器來決定,而開發者只需專註於業務邏輯。
但是,對於iOS開發來說,記憶體管理是個很重要的概念,如果先要寫出記憶體使用效率高而又沒有bug的代碼,就得掌握其記憶體管理模型的細節。
一、引用計數
1.與記憶體管理的關係?
在Objective-C記憶體管理中,每個對象都有屬於自己的計數器:如果想讓某個對象繼續存活(例如想對該對象進行引用),就遞增它的引用計數;當用完它之後,就遞減該計數;當沒人引用該對象,它的計數變為0之後,系統就把它銷毀。
這個,就是引用計數在其中充當的角色:用於表示當前有多少個對象想令此對象繼續存活程式中;
2.引用計數的介紹:
引用計數(Reference Count),也叫保留計數(retain count),表示對象被引用的次數。一個簡單而有效的管理對象生命周期的方式。
3.引用計數的工作原理:
- 當我們創建(alloc)一個新對象A的時候,它的引用計數從零變為 1;
- 當有一個指針指向這個對象A,也就是某對象想通過引用保留(retain)該對象A時,引用計數加 1;
- 當某個指針/對象不再指向這個對象A,也就是釋放(release)該引用後,我們將其引用計數減 1;
- 當對象A的引用計數變為 0 時,說明這個對象不再被任何指針指向(引用)了,這個時候我們就可以將對象A銷毀,所占記憶體將被回收,且所有指向該對象的引用也都變得無效了。系統也會將其占用的記憶體標記為“可重用”(reuse);
流程參考圖如下:
(圖片表格取自《編寫高質量iOS與OS X代碼的52個有效方法》一書)
4.操作引用計數的方法:
A.以下是NSObject協議中聲明的3個用於操作計數器的方法:
- retain : 保留。保留計數+1;
- release : 釋放。保留計數 -1;
- autorelease :稍後(清理“自動釋放池”時),再遞減保留計數,所以作用是延遲對象的release;
B.dealloc方法:另外,當計數為0的時候對象會自動調用dealloc。而我們可以在dealloc方法做的,就是釋放指向其他對象的引用,以及取消已經訂閱的KVO、通知;(自己不能調用dealloc方法,因為運行期系統會在恰當的時候調用它,而且一旦調用dealloc方法,對象不再有效,即使後續方法再次調用retain。)
所以,調用release後會有2種情況:
調用前計數>1,計數減1;
調用前計數<1,對象記憶體被回收;
C.retainCount:獲取引用計數的方法。
Eg: [object retainCount]; //得到object的引用計數
retain、release、autorelease詳解:
retain作用:
調用後計數+1,保留對象操作。但是當對象被銷毀、記憶體被回收的時候,即使使用retain也不再有效;
autorelease作用:
autorelease不立即釋放,而是註冊到autoreleasepool(自動釋放池)中,等到pool結束時釋放池再自動調用release進行釋放工作。
autorelease看上去很像ARC,但是實際上更類似C語言中的自動變數(局部變數),當某自動變數超出其作用域(例如大括弧),該自動變數將被自動廢棄,而autorelease中對象實例的release方法會被調用;[與C不同的是,開發者可以設定變數的作用域。]
釋放時間:每個Runloop中都創建一個Autorelease pool(自動釋放池),每一次的Autorelease,系統都會把該Object放入了當前的Autorelease pool中,併在Runloop的末尾進行釋放,而當該pool被釋放時,該pool中的所有Object會被調用Release。 所以,一般情況下,每個接受autorelease消息的對象,都會在下個Runloop開始前被釋放。
例如可用以下場景:(需要從ARC改為使用手動管理的可以做如下的設置: 在Targets的Build Phases選項下Compile Sources下選擇要不使用ARC編譯的文件,雙擊它,輸入-fno-objc-arc即可使用MRC手工管理記憶體方式;)
-(NSString *)getSting
{
NSString *str = [[NSString alloc]initWithFormat:@"I am Str"];
return [str autorelease];
}
自動釋放池中的釋放操作會等到下一次時間迴圈時才會執行,所以調用以下:
NSString *str = [self getSting];
NSLog(@"%@",str);
返回的str對象得以保留,延遲釋放。因此可以無需再NSLog語句之前執行保留操作,就可以將返回的str對象輸出。
所以可見autorelease的作用是能延長對象的生命期。使其在跨越方法調用邊界後依然可以存活一段時間。
release作用:
release會立即執行釋放操作,使得計減1;
有這樣一種情況:當某對象object的引用計數為1的時候,調用“[object release];”,此時如果再調用NSLog方法輸出object的話,可能程式就會崩潰,當然只是有可能,因為對象所占記憶體在“解除分配(deallocated)”之後,只是放回“可用記憶體池(avaiable pool)”,但是如果執行NSLog時,尚未覆寫對象記憶體,那麼該對象依然有效,所以程式有可能不會崩潰,由此可見,因過早地釋放對象而導致的bug很難調試。
為避免這種情況,一般調用完對象之後都會清空指針:"object = nil",這樣就能保證不會出現指向無效對象的指針,也就是懸掛指針(dangling pointer);
懸掛指針:指向無效對象的指針。
那麼,向已經釋放(dealloc)的對象發送消息,retainCount會是多少?
原則是不可以這麼做。因為該對象的記憶體已經被回收,而我們向一個已經被回收的對象發了一個 retainCount 消息,所以它的輸出結果應該是不確定的,例如為減少一次記憶體的寫操作,不將這個值從 1 變成 0,所以很大可能輸出1。例如下麵這種情況:
Person *person = [[Person alloc] init]; //此時,計數 = 1
[person retain]; //計數 = 2
[person release]; //計數 = 1
[person release]; //很可能計數 = 1;
雖然第四行代碼把計數1release了一次,原理上person對象的計數會變成0,但是實際上為了優化對象的釋放行為,提高系統的工作效率,在retainCount為1時release系統會直接把對象回收,而不再為它的計數遞減為0,所以一個對象的retainCount值有可能永遠不為0;
因此,不管是否為ARC的開發環境中,也不推薦使用retainCount來做為一個對象是否存在於記憶體之中的依據。
二、ARC
1.背景:
ARC是iOS 5推出的新功能,全稱叫 ARC(Automatic Reference Counting)。
即使2014 年的 WWDC 大會上推出的Swift 語言,該語言仍然使用 ARC 技術作為其管理方式。
2.ARC是什麼?
需要註意的是,ARC並不是GC(Garbage Collection 垃圾回收器),它只是一種代碼靜態分析(Static Analyzer)工具,背後的原理是依賴編譯器的靜態分析能力,通過在編譯時找出合理的插入引用計數管理代碼,從而提高iOS開發人員的開發效率。
Apple的文檔里是這麼定義ARC的:
“自動引用計數(ARC)是一個編譯器級的功能,它能簡化Cocoa應用中對象生命周期管理(記憶體管理)的流程。”
3.ARC在做什麼?
在編譯階段,編譯器將在項目代碼中自動為分配對象插入retain、release和autorelease,且插入的代碼不可見。
但是,需要註意的是,ARC模式下引用計數規則還起作用,只是編譯器會為開發者分擔大部分的記憶體管理工作,除了插入上述代碼,還有一部分優化以及分析記憶體的管理工作。
作用:
- a.降低記憶體泄露等風險 ;
- b.減少代碼工作量,使開發者只需專註於業務邏輯;
4.ARC具體為引用計數做了哪些工作?
編譯階段自動添加代碼:
編譯器會在編譯階段以恰當的時間與地方給我們填上原本需要手寫的retain、release、autorelease等記憶體管理代碼,所以ARC並非運行時的特性,也不是如java中的GC運行時的垃圾回收系統;因此,我們也可以知道,ARC其實是處於編譯器的特性。
例如:
-(void)setup
{
_person = [person new];
}
在手工管理記憶體的環境下,_person是不會自動保留其值,而在ARC下編譯,其代碼會變成:
-(void)setup
{
person *tmp = [person new];
_person = [tmp retain];
[tmp release];
}
當然,在開發工作中,retain和release對於開發人員來說都可以省去,由ARC系統自動補全,達到同樣的效果。
但實際上,ARC系統在自動調用這些方法時,並不通過普通的Objective-C消息派發控制,而是直接調用底層C語言的方法:
比如retain,ARC在分析到某處需要調用保留操作的地方,調用了與retain等價的底層函數 objc_retain,所以這也是ARC下不能覆寫retain、release或者autorelease的原因,因為這些方法在ARC從來不會被直接調用。
運行期組件的優化:
ARC是編譯器的特性,但也包含了運行期組件,所執行的優化很有意義。
例子:
person工廠方法personWithName可以得到一個person對象,在這裡調用並賦值給person的一個實例_one:
_one = [person personWithName:@"name"];
可能會出現這種情況:
在personWithName方法中,返回對象給_one之前,為其調用了一次autorelease方法。
由於實例變數是個強引用,所以編譯器會在設置其值的時候還需要執行一次保留操作。
person *tmp = [person personWithName:@"name"]; //在personWithName方法返回前已有調用一次autorelease方法進行保留操作;
_one = [tmp retain];
很明顯,autorelease與緊跟其後的retain是重覆的。為提升性能,可以將二者刪去,捨棄autorelease這個概念,並且規定返回對象的技術都比期望值多1,但是為了向後相容非ARC等情況,ARC採取另外一種方式:
ARC可以在運行期檢測到這一對多餘的操作。
- 返回對象時,不直接調用autorelease,改為調用objc_autoreleaseReturnValue,用來檢測返回之後即將要執行的代碼中,含有retain操作,則設置全局數據結構(此數據結構具體內容因處理器而異)中的一個標誌位,而不執行autorelease操作。
- 同樣,若方法返回一個自動釋放對象,調用personWithName方法的代碼段不執行retain,改為執行objc_retainAutoreleaseReturnValue函數。此函數檢測剛纔的那個標誌位,若已經置位了,則不執行retain操作。
而,設置並檢測標誌位,要比調用autorelease和retain更快,這就使得這一情況的處理得到優化。
修改2個函數後優化完整結果如下: 【例子來自《編寫高質量iOS與OS X代碼的52個有效方法》一書P126】
我們可以通過兩個函數的偽代碼大致描述如下:
像是objc_autoreleaseReturnValue這個函數是如何檢測方法調用者是否會立刻保留對象呢,這就要交給處理器來解決了。
由於必須查看原始機器碼指令方可判斷出這一點需要處理器來定。
所以,其實只有編譯器的作者才能知道這裡是如何實現此函數的。
ARC的安全性:
在編寫屬性的設置方法(setter)時,如果使用手工管理方式,可能會需要如下編寫:
-(void)setObject:(id)object
{
[_object release];
_object = [object retain];
}
但是這樣寫會出現問題:如果說新值object和實例變數_object的值是相同的,而且只有當前實例變數對象還在引用這個值,那麼設置方法中的釋放操作會使得該值保留計數為0,系統將其回收,所以接下來的保留操作,將會令應用程式崩潰。
而在使用ARC的環境下,就不可能會發送這樣的的“邊界情況”了:
剛纔的代碼在ARC下可以這樣寫(當然,我們知道如果不需要覆寫setter方法,也可以不編寫此方法,直接使用"self.object = xxx"也可以安全地調用。):
-(void)setObject:(id)object
{
_object = object;
}
而且ARC會用一種安全的方式來設置:先保留新值,再釋放舊值,最後設置實例變數。
在手工管理的情況下,我們需要特別註意這種"邊緣情況",但是ARC下,我們就可以很輕鬆地編寫這種代碼了,而不用去考慮這種情況如何處理了。
總結:將記憶體管理交由編譯器和運行期組件來做,可以使代碼得到多種優化,而上面是其中一種方式。
5.ARC下需要註意的規則
不能顯式調用以下代碼:
(NSZone:記憶體區)
不能再使用NSAutoreleasePool對象,ARC提供了@autoreleasepool塊來代替它,這樣更有效率;
關於dealloc:
- 不能顯式調用dealloc;
- 不能再dealloc中調用【super dealloc】(非ARC下則需要調用.);
- 不能在dealloc 中釋放資源(非ARC下需要釋放不同的對象);
6.所有權修飾符
oc編程中為了處理對象,可將變數類型定義為id類型或各種對象類型。使用這些限定符可以確切地聲明對象變數和屬性的生命周期;
所謂對象類型就是指向NSObject這樣的oc類的指針,例如“NSObject *”。id類型用於隱藏對象類型的類名部分。相當於C語言中常用的“void *”;
ARC下,id類型和對象類型上必須附加所有權修飾符;
所有權修飾符一共有4種:
__strong:
強引用,可以引用別的對象為強引用,相當於retain的特性;表明變數持有alloc/new/copy/mutableCopy方法群創建的對象的強引用,強引用變數會在其作用域里被保留,在超出作用域後被釋放,為預設的修飾符;
例如以下代碼:
id objc = [[NSObject alloc] init];
實際上已被附上所有權修飾符:
id __strong objc = [[NSObject alloc] init];
__weak:
使用__strong,有可能2個對象相互強引用或者1個對象對自身強引用則會發生迴圈引用(如下圖,或者叫保留環),所以當對象在超出其生存周期後,本應被系統廢棄卻仍然被引用者所持有,所以造成記憶體泄露(應當廢棄的對象在超出生命周期後,繼續存在);
而當我們對可能會發送迴圈引用的對象進行__weak弱引用修飾,弱引用變數不會持有對象,且生成的對象會立刻釋放,可避免迴圈引用,並且弱引用還有另外一個特點,若對象被系統回收,該弱引用變數將自動失效並且賦值為nil。
__unsafe_unretained: 不安全的所有權徐師傅,ARC的記憶體管理是編譯器的工作,而附有__unsafe_unretained修飾符的變數不屬於編譯器的記憶體管理對象。與__weak作用一樣,也可以避免迴圈引用;但是不同的是,__unsafe_unretained屬性的變數不會將變數設置為nil,而是就處於於懸掛狀態;
__autoreleasing:在ARC中使用“@autoreleasepool塊”來取代“NSAutoreleasePool”類對象的生成,通過將對象賦值給附加了__autoreleasing修飾符的變數來替代調用autorelease方法;
Other:ARC需要註意的事項?
1.過度使用 block 之後,無法解決迴圈引用問題。
2.遇到底層 Core Foundation 對象,需要自己手工管理它們的引用計數時,我們需轉換關鍵字,作為橋接轉換以解決 Core Foundation 對象與 Objective-C 對象相對轉換的問題:
__bridge:使用__bridge標記可以在不修改相關對象的引用計數的情況下,將對象從Core Foundation框架數據類型轉換為Foundation框架數據類型(反之亦然)。
__bridge_retained:會將相關對象的引用計數加 1,並且可以將Core Foundation框架數據類型對象轉換為Foundation框架數據類型對象,並從ARC接管對象的所有權。
__bridge_transfer:可以將Foundation框架數據類型對象轉換為Core Foundation框架數據類型對象,並且會將對象的所有權交給ARC管理,也就是說引用計數交由ARC管理;
總結:就推薦2本經典的書(估計很多人早就看完了