iOS開發--引用計數與ARC

如下是關於內存管理的學習筆記:引用計數與ARC。

iOS5之前自動引用計數(ARC)是在MacOS X 10.7與iOS 5中引入一項新技術,用於代替以前的手工引用計數MRC(Manual Reference Counting)管理Objective-C中的對象【官方也叫MRR(Manual Retain Release)】。現在,ARC下的iOS項目幾乎把全部內存管理事宜都交給編譯器來決定,而開發者只需專一於業務邏輯。

可是,對於iOS開發來講,內存管理是個很重要的概念,若是先要寫出內存使用效率高而又沒有bug的代碼,就得掌握其內存管理模型的細節。

1、引用計數

1.與內存管理的關係?

在Objective-C內存管理中,每一個對象都有屬於本身的計數器:若是想讓某個對象繼續存活(例如想對該對象進行引用),就遞增它的引用計數;當用完它以後,就遞減該計數;當沒人引用該對象,它的計數變爲0以後,系統就把它銷燬。 html

這個,就是引用計數在其中充當的角色:用於表示當前有多少個對象想令此對象繼續存活程序中;java

 

2.引用計數的介紹:編程

引用計數(Reference Count),也叫保留計數(retain count),表示對象被引用的次數。一個簡單而有效的管理對象生命週期的方式。安全

 

3.引用計數的工做原理:數據結構

  1. 當咱們建立(alloc)一個新對象A的時候,它的引用計數從零變爲 1;
  2. 當有一個指針指向這個對象A,也就是某對象想經過引用保留(retain)該對象A時,引用計數加 1;
  3. 當某個指針/對象再也不指向這個對象A,也就是釋放(release)該引用,咱們將其引用計數減 1;
  4. 當對象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;oop

調用前計數<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來作爲一個對象是否存在於內存之中的依據。

 

 


 

2、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能夠在運行期檢測到這一對多餘的操做。

  1. 返回對象時,不直接調用autorelease,改成調用objc_autoreleaseReturnValue,用來檢測返回以後即將要執行的代碼中,含有retain操做,則設置全局數據結構(此數據結構具體內容因處理器而異)中的一個標誌位,而不執行autorelease操做。
  2. 一樣,若方法返回一個自動釋放對象,調用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本經典的書(估計不少人早就看完了😂 ),書本也好,pdf也好,建議看一下:

《Effective Objective-C 2.0  編寫高質量iOS與OS X代碼的52個有效方法》

《Objective-C高級編程 iOS與OS X多線程和內存管理》

相關文章
相關標籤/搜索