iOS開發– ARC與引用計數

啊左~發表於2016-11-23

以下是關於記憶體管理的學習筆記: 引用計數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);

流程參考圖如下:

111707017-089e69ea2340eaa6

(圖片表格取自《編寫高質量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;
呼叫前計數

C.retainCount:獲取引用計數的方法。

Eg: [object retainCount]; //得到object的引用計數

retain、autorelease、release方法詳解

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手工管理記憶體方式;)

自動釋放池中的釋放操作會等到下一次時間迴圈時才會執行,所以呼叫以下:

返回的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。例如下面這種情況:

雖然第四行程式碼把計數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其實是處於編譯器的特性。
例如:

在手工管理記憶體的環境下,_person是不會自動保留其值,而在ARC下編譯,其程式碼會變成:

當然,在開發工作中,retain和release對於開發人員來說都可以省去,由ARC系統自動補全,達到同樣的效果。
但實際上,ARC系統在自動呼叫這些方法時,並不通過普通的Objective-C訊息派發控制,而是直接呼叫底層C語言的方法:
比如retain,ARC在分析到某處需要呼叫保留操作的地方,呼叫了與retain等價的底層函式 objc_retain,所以這也是ARC下不能覆寫retain、release或者autorelease的原因,因為這些方法在ARC從來不會被直接呼叫。

執行期元件的優化:

ARC是編譯器的特性,但也包含了執行期元件,所執行的優化很有意義。
例子:
person工廠方法personWithName可以得到一個person物件,在這裡呼叫並賦值給person的一個例項_one:

可能會出現這種情況:
在personWithName方法中,返回物件給_one之前,為其呼叫了一次autorelease方法。
由於例項變數是個強引用,所以編譯器會在設定其值的時候還需要執行一次保留操作。

很明顯,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】

121707017-1cabca8cb7529ecf

我們可以通過兩個函式的虛擬碼大致描述如下:

131707017-f91c499eb0df48dd
141707017-84f960ffbe26f4bc

像是objc_autoreleaseReturnValue這個函式是如何檢測方法呼叫者是否會立刻保留物件呢,這就要交給處理器來解決了。
由於必須檢視原始機器碼指令方可判斷出這一點需要處理器來定。
所以,其實只有編譯器的作者才能知道這裡是如何實現此函式的。
ARC的安全性:
在編寫屬性的設定方法(setter)時,如果使用手工管理方式,可能會需要如下編寫:

但是這樣寫會出現問題:如果說新值object和例項變數_object的值是相同的,而且只有當前例項變數物件還在引用這個值,那麼設定方法中的釋放操作會使得該值保留計數為0,系統將其回收,所以接下來的保留操作,將會令應用程式崩潰。而在使用ARC的環境下,就不可能會傳送這樣的的“邊界情況”了:

剛才的程式碼在ARC下可以這樣寫:(當然,我們知道如果不需要覆寫setter方法,也可以不編寫此方法,直接使用”self.object = xxx”也可以安全地呼叫。):

而且ARC會用一種安全的方式來設定:先保留新值,再釋放舊值,最後設定例項變數。
在手工管理的情況下,我們需要特別注意這種”邊緣情況”,但是ARC下,我們就可以很輕鬆地編寫這種程式碼了,而不用去考慮這種情況如何處理了。
總結:將記憶體管理交由編譯器執行期元件來做,可以使程式碼得到多種優化,而上面是其中一種方式。

5.ARC下需要注意的規則

a.關於dealloc:
. 不能顯式呼叫dealloc;
. 不能再dealloc中呼叫【super dealloc】(非ARC下則需要呼叫.);
. 不能在dealloc 中釋放資源(非ARC下需要釋放不同的物件);
b.以及,不能顯式呼叫以下程式碼:

151707017-ae407454e2886d04

(NSZone:記憶體區)

c.不能再使用NSAutoreleasePool物件,ARC提供了@autoreleasepool塊來代替它,這樣更有效率;

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個物件對自身強引用則會發生迴圈引用(如下圖,或者叫保留環),所以當物件在超出其生存週期後,本應被系統廢棄卻仍然被引用者所持有,所以造成記憶體洩露(應當廢棄的物件在超出生命週期後,繼續存在);

161707017-5923f7ba172d2c7e

(2個物件互相強引用)

171707017-d641bfcf412d9196

(某物件對自身強引用)

而當我們對可能會傳送迴圈引用的物件進行__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本經典的書(估計很多人早就看完了? ),書本也好,pdf也好,建議看一下:
《Effective Objective-C 2.0 編寫高質量iOS與OS X程式碼的52個有效方法》
《Objective-C高階程式設計 iOS與OS X多執行緒和記憶體管理》

 


(轉載請標明原文出處,謝謝支援 ~ ^-^ ~)
 by:啊左~

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

iOS開發– ARC與引用計數 iOS開發– ARC與引用計數

相關文章