ARC機制

iPhone開發深入淺出 — ARC

 

 本文摘自“泰然”論壇

一、ARC是什麼

         ARC是iOS 5推出的新功能,全稱叫 ARC(Automatic Reference Counting)。簡單地說,就是代碼中自動加入了retain/release,原先需要手動添加的用來處理內存管理的引用計數的代碼可以自動地由編譯器完成了。

該機能在 iOS 5/ Mac OS X 10.7 開始導入,利用 Xcode4.2 可以使用該機能。簡單地理解ARC,就是通過指定的語法,讓編譯器(LLVM 3.0)在編譯代碼時,自動生成實例的引用計數管理部分代碼。有一點,ARC並不是GC,它只是一種代碼靜態分析(Static Analyzer)工具。

 

變化點

通過一小段代碼,我們看看使用ARC前後的變化點

1. @interface NonARCObject : NSObject {

2.     NSString *name;

3. }

4. -(id)initWithName:(NSString *)name;

5. @end

6.  

7. @implementation NonARCObject

8. -(id)initWithName:(NSString *)newName {

9.     self = [super init];

10.              if (self) {

11.                  name = [newName retain];

12.              }

13.              return self;

14.          }

15.           

16.          -(void)dealloc {

17.              [name release];

18.              [Super dealloc];

19.          }

20.          @end

1. @interface ARCObject : NSObject {

2.     NSString *name;

3. }

4. -(id)initWithName:(NSString *)name;

5. @end

6.  

7. @implementation ARCObject

8. -(id)initWithName:(NSString *)newName {

9.     self = [super init];

10.              if (self) {

11.                  name = newName;

12.              }

13.              return self;

14.          }

15.          @end

我們之前使用Objective-C中內存管理規則時,往往採用下面的準則

 

•  生成對象時,使用autorelease

•  對象代入時,先autorelease後再retain

•  對象在函數中返回時,使用:

return [[object retain]autorelease];

而使用ARC後,我們可以不需要這樣做了,甚至連最基礎的release都不需要了

使用ARC的好處

使用ARC有什麼好處呢

•  看到上面的例子,大家就知道了,以後寫Objective-C的代碼變得簡單多了,因爲我們不需要擔心煩人的內存管理,擔心內存泄露了

•  代碼的總量變少了,看上去清爽了不少,也節省了勞動力

•  代碼高速化,由於使用編譯器管理引用計數,減少了低效代碼的可能性

 

不好的地方

•  記住一堆新的ARC規則、關鍵字及特性等需要一定的學習週期

•  一些舊的代碼,第三方代碼使用的時候比較麻煩;修改代碼需要工數,要麼修改編譯開關

 

關於第二點,由於 XCode4.2 中缺省ARC就是 ON 的狀態,所以編譯舊代碼的時候往往有”Automatic Reference CountingIssue”的錯誤信息。


這個時候,可以將項目編譯設置中的“Objectice-C Auto Reference Counteting”設爲NO。如下所示。


如果只想對某個.m文件不適應ARC,可以只針對該類文件加上 -fno-objc-arc 編譯FLAGS,如下圖。


ARC基本規則

•  retain, release, autorelease, dealloc由編譯器自動插入,不能在代碼中調用.

•  dealloc雖然可以被重載,但是不能調用[super dealloc].

 

由於ARC並不是GC,並需要一些規則讓編譯器支持代碼插入,所以必須清楚清楚了這些規則後,才能寫出健壯的代碼

Objective-C對象

ObjectiveC中的對象,有強參照(Strong reference)和弱參照(Weak reference)之分,當需要保持其他對象的時候,需要retain以確保對象引用計數加1。對象的持有者(owner)只要存在,那麼該對象的強參照就一直存在。

對象處理的基本規則是

 

•  只要對象的持有者存在(對象被強參照),那麼就可以使用該對象

•  對象失去了持有者後,即被破棄強參照 (Strong reference)


(s1)firstName作爲”natsu”字符串對象的最初持有者,是該NSString類型對象的Strong reference。

 

(s2)這裏將firstName代入到aName中,即aName也成爲了@”natsu”字符串對象的持有者,對於該對象,aName也是Strong reference。

 

(s3)這裏,改變firstName的內容。生成新的字符串對象”maki”。這時候firstName成爲”maki”的持有者,而@”natsu”的持有者只有aName。每個字符串對象都有各自的持有者,所以它們都在內存中都存在。

 

(s4)追加新的變量otherName, 它將成爲@”maki”對象的另一個持有者。即NSString類型對象的Strong reference。

 

(s5)將otherName代入到aName,這時,aName將成爲@”maki”字符串對象的持有者。而對象@”natsu”已經沒有持有者了,該對象將被破棄。

 

弱參照 (Weak reference)

接下來我們來看看弱參照 (Weak reference) 的使用方式。


(w1)與強參照方式同樣,firstName作爲字符串對象@”natsu”的持有者存在。即是該NSString類型對象的Strong reference。

 

(w2)使用關鍵字__weak,聲明弱參照weakName變量,將firstName代入。這時weakName雖然參照@”natsu”,但仍是Weakreference。即weakName雖然能看到@”natsu”,但不是其持有者。

 

(w3)firstName指向了新的對象@”maki”,成爲其持有者,而對象@”natsu”因爲沒有了持有者,即被破棄。同時weakName變量將被自動代入nil。

 

引用關鍵字

ARC中關於對象的引用參照,主要有下面幾關鍵字。使用strong, weak, autoreleasing限定的變量會被隱式初始化爲nil

__strong

變量聲明缺省都帶有__strong關鍵字,如果變量什麼關鍵字都不寫,那麼缺省就是強參照

 

__weak

上面已經看到了,這是弱參照的關鍵字。該概念是新特性,從 iOS 5/ Mac OS X 10.7 開始導入。由於該類型不影響對象的生命週期,所以如果對象之前就沒有持有者,那麼會出現剛創建就被破棄的問題,比如下面的代碼。

1. NSString __weak *string = [[NSString alloc] initWithFormat:@"First Name: %@", [self firstName]];

2. NSLog(@"string: %@", string); //此時 string爲空

 

如果編譯設定OS版本 Deployment Target 設定爲這比這低的版本,那麼編譯時將報錯(The current deployment target does not support automated __weak references),這個時候,我們可以使用下面的__unsafe_unretained。

弱參照還有一個特徵,即當參數對象失去所有者之後,變量會被自動賦值上nil (Zeroing)。

__unsafe_unretained

該關鍵字與__weak一樣,也是弱參照,與__weak的區別只是是否執行nil賦值(Zeroing)。但是這樣,需要注意變量所指的對象已經被破棄了,地址還還存在,但內存中對象已經沒有了。如果還是訪問該對象,將引起「BAD_ACCESS」錯誤。

__autoreleasing

該關鍵字使對像延遲釋放。比如你想傳一個未初始化的對象引用到一個方法當中,在此方法中實例化此對象,那麼這種情況可以使用__autoreleasing。他被經常用於函數有值參數返回時的處理,比如下面的例子。

1. - (void) generateErrorInVariable:(__autoreleasing NSError **)paramError {

2.     ....

3.     *paramError = [[NSError alloc] initWithDomain:@"MyApp" code:1 userInfo:errorDictionary];

4. }

5.  

6. ....

7. {

8.     NSError *error = nil;

9.     [self generateErrorInVariable:&error];

10.              NSLog(@"Error = %@", error);

11.          }

又如函數的返回值是在函數中申請的,那麼希望釋放是在調用端時,往往有下面的代碼。

1. -(NSString *)stringTest

2. {

3.     NSString *retStr = [NSString stringWithString:@"test"];

4.  

5.     return [[retStr retain] autorelease];

6. }

7.  

8. // 使用ARC

9.  

10.          -(NSString *)stringTest

11.          {

12.              __autoreleasing NSString *retStr = [NSString alloc] initWithString:@"test"];

13.           

14.              return retStr;

15.          }

即當方法的返回參數是id*,且希望方法返回時對象被autoreleased,那麼使用該關鍵字。

總結

今天,我們看到了基本的ARC使用規則

 

•  代碼中不能使用retain, release, retain, autorelease

•  不重載dealloc(如果是釋放對象內存以外的處理,是可以重載該函數的,但是不能調用[super dealloc])

•  不能使用NSAllocateObject, NSDeallocateObject

•  不能在C結構體中使用對象指針

•  id與void *間的如果cast時需要用特定的方法(__bridge關鍵字)

•  不能使用NSAutoReleasePool、而需要@autoreleasepool塊

•  不能使用“new”開始的屬性名稱 。(如果使用會有下面的編譯錯誤”Property’s synthesized getter follows Cocoa naming convention for returning ‘owned’ objects”)

今後,我們將更加深入ARC,學習其更多的特性

 

 

 

 

 

 

 

 

 

 

 

二、ARC對@property的使用規則影響

 

上一回我們學到了一些ARC的基本概念,這一次我們來看看ARC對@property的使用規則有何影響

所有者屬性

我們先來看看與所有權有關係的屬性,關鍵字間的對應關係。

屬性值

關鍵字

所有權

strong

__strong

weak

__weak

unsafe_unretained

__unsafe_unretained

copy

__strong

assign

__unsafe_unretained

retain

__strong

 

strong

該屬性值對應 __strong 關鍵字,即該屬性所聲明的變量將成爲對象的持有者。

 

weak

該屬性對應 __weak 關鍵字,與 __weak 定義的變量一致,該屬性所聲明的變量將沒有對象的所有權,並且當對象被破棄之後,對象將被自動賦值nil。並且,delegate 和 Outlet 應該用 weak 屬性來聲明。同時,如上一回介紹的 iOS 5 之前的版本是沒有 __weak 關鍵字的,所以 weak 屬性是不能使用的。這種情況我們使用 unsafe_unretained。

 

unsafe_unretained

等效於__unsafe_unretaind關鍵字聲明的變量;像上面說明的,iOS 5之前的系統用該屬性代替 weak 來使用。

copy

與 strong 的區別是聲明變量是拷貝對象的持有者。

 

assign

一般Scalar Varible用該屬性聲明,比如,int, BOOL。

 

retain

該屬性與 strong 一致;只是可讀性更強一些。

 

讀寫相關的屬性 (readwrite, readonly)

讀寫相關的屬性有 readwrite 和 readonly 兩種,如果使用ARC之後,我麼需要注意一下 readonly 屬性的使用。

比如下面的變量聲明。

21.          @property (nonatomic, readonly) NSString *name;

一般聲明爲 readonly 的變量按理說應該不需要持有所有權了,但是在ARC有效的情況下,將出現下面的錯誤信息:“ARC forbids synthesizing a property of an Objective-C object with unspecified ownership or storage attribute”( ARC禁止合成一個objective - c對象所有權或存儲屬性不明的屬性)

如果定義了ARC有效,那麼必須要有所有者屬性的定義;所以我們的代碼改成這樣,就OK了.

16.          @property (nonatomic, strong, readonly) NSString *name;

不過有一點,Scalar Varible的變量缺省都有 assign 的屬性定義,所以不需要給他們單獨的明示聲明了。

 

 

 

ARC產生之前的 Objective-C 內存管理

 

前兩節我們對 ARC(Automatic Reference Counting) 有了一個基本的理解,但是 ARC 是怎麼產生的,爲什麼蘋果要在其最新的 iOS/Mac OS X 上導入該框架? 如果不理解其背後的基本原理,只是死記硬背那些規則/方法,是毫無意義的。就像我們從小接受的填鴨式教育,基本上到後來都還給老師了。

本節,我們先來看看 ARC 產生之前的 Objective-C 內存管理世界,然後再來看看導入 ARC 後,新的 LLVM 編譯器在背後爲我們做了什麼。

 

Objective-C 內存管理

和許多面嚮對象語言一樣,Objective-C 中內存管理的方式其實就是指 引用計數 (Reference Counting)的使用準則。如下圖所示,對象生成的時候必定被某個持有者拿着,如果有多個持有者的話,其引用計數就會遞增;相反失去一個持有者那麼引用計數即會遞減,直到失去所有的持有者,才真正地從內存中釋放自己。


 

基本原則

內存管理的依循下面的基本原則

 

1、自己生成的對象,那麼即是其持有者。不是自己生成的對象,也可成爲其持有者(一個對象可以被多個人持有)如果不想持有對象的時候,必須釋放其所有權。不能釋放已不再持有所有權的對象

結合 Objective-C 語言中的方法,我們來看看基本的內存管理。

方法

動作

alloc/new/copy/mutableCopy

生成對象並擁有所有權

retain

擁有對象所有權

release

釋放對象所有權

dealloc

釋放對象資源

 

實際上這些函數並不能說是 Objective-C 語言所特有的,而是 OS X / iOS 系統庫中包含的基類函數;具體說就是 Cocoa Framework::Foundation::NSObject 基類的成員函數。

Objective-C 語言內部嚴格遵守上面表格中的定義;首先是 alloc/new/copy/mutableCopy 這幾個函數,並且是alloc/new/copy/mutableCopy 開頭的函數,比如:allpcMyObject/newTheObject/copyThis/mutableCopyTheObject 等都必須遵循這個原則。

反而言之,如果不是 alloc/new/copy/mutableCopy 開頭的函數,而且要返回對象的話,那麼調用端只是生成對象,而不是其持有者。

17.          -(id)allocObject {

18.              /*

19.               * 生成對象並擁有所有權

20.               */

21.              id obj = [[NSObject alloc] init];

22.           

23.              /*

24.               * 自己一直是持有對象狀態

25.               */

26.              return obj;

27.          }

如上面的例子,alloc 生成的對象,其所有權會傳遞給函數的調用端;即滿足了 alloc 開頭函數的命名規則。

再看下面的例子

 

•   -(id)object {

•       id obj = [[NSObject alloc] init];

•    

•       /*

•        * 自己一直是持有對象狀態

•        */

•    

•       [obj autorelease];

•    

•       /*

•        * 對象還存在,只是並不持有它的所有權

•        */

•    

•       return obj;

•   }

這裏我們用到了 autorelease 函數。它的作用既是將對象放入 NSAutoreleasePool 中,由其來維護其生命週期。換句話說對象的持有者是 NSAutoreleasePool;上面的例子中,object 返回後,調用者將不持有其所有權。(除非再調用 retain。)

用 autorelease 的一個理由既是讓程序員來控制對象的存活週期,而不像 C/C++ 等語言中,出棧後,棧中數據都被自動廢棄,或者用 { } 框住的自動變量,當出了範圍就看不到了。在 Objective-C 中,只有當 [pool drain] 被調用的時候,才清空 pool 中所有登記的對象實體,在這之前,你可以像往常一樣正常使用對象。

當然可以想象得到的,如果一個程序只有一個 NSAutoreleasePool,並在 main 中聲明,程序結束時才 [pool drain]/[pool release] 的話,那麼所有 autorelease 的對象都將塞滿這個 pool,會耗掉系統大部分內存。所以,使用 NSAutoreleasePool 的時候也儘量建議局部使用,比如下面的循環。

•   for (i=0; i < 100; i++) {

•       NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

•    

•       // 下面的函數由於不屬於 alloc/new/copy/mutableCopy 範疇的函數,所以都使用了 autorelease

•       NSMutableArray* array = [NSMutableArray array];

•       NSString *str = [NSString stringWithFormat:@"TestCode"];

•    

•       /*

•        * 其他使用autorelease定義的對象

•        */

•       Test *test = [[[Test alloc] init] autorelease];

•    

•       // 通過下面的函數,可以隨時監控pool中的對象

•       // iOS以外的運行庫的情況下,也可以使用 _objc_autoreleasePoolPrint() 私有函數,只是需要下面的聲明

•       // extern void _objc_autoreleasePoolPrint();

•       [NSAutoreleasePool showPools];

•    

•       // 這裏把所有pool中的對象都釋放掉

•       [pool release];

•   }

當然 NSAutoreleasePool 也可以嵌套,基本上都依存大括號規則。

編程準則

基於以上原則,在 ARC 誕生之前,我們往往用下面準則來寫代碼。

生成對象時,使用autorelease

一般情況下,我們這樣生成對象並使用

•   MyController* controller = [[MyController alloc] init];

•   // ......

•   [controller release];

如果在 [controller release] 之前函數return了怎麼樣,內存泄露了;爲了防患於未然,一般像下面一樣 生成對象時,使用autorelease。這樣一來,該對象就被自動加入到最近的那個 pool 中。

•   MyController* controller = [[[MyController alloc] init] autorelease];

 

對象代入時,先autorelease後再retain

對象代入的時候,如果之前不將變量所持有的對象釋放,那麼很可能引起內存泄露。比如下面的代碼

•   {

•     _member = [[TempValue alloc] init];

•   }

•    

•   - (void)setValue:(TempValue *)value {

•     _member = value;

•     // 這時,之前持有的對象因爲沒有 release 而引起內存泄露

•     // 當然,先 [_member release] 後再代入也是可以的,

•     // 但是當與「對象在函數中返回時」的問題一同考慮時,

•     // 如果沒有 return [[object retain] autorelease] 的保證,這裏即使 [_member release]也是百搭

•     // 詳細的解釋見下

•   }

鑑於以上原因,我們將原先的對象先autorelease後再將新對象retain代入

3. {

4.   _member = [[TempValue alloc] init];

5.   // 這裏,即使使用【生成對象時,使用autorelease】的準則,也沒有關係

6.   // 使用autorelease一次就將制定對象放入pool中,放幾次[pool drain]的時候就釋放幾次

7. }

8.  

9. - (void)setValue:(TempValue *)value {

10.            [_member autorelease];

11.            _member = [value retain];

12.          }

該原則遵循 Failed Self 的原則,雖然從性能上看有所損耗但是保證了代碼質量。

對象在函數中返回時

使用return [[object retain] autorelease]

嚴格地說,是除 alloc/new/copy/mutableCopy 開頭函數以外的函數中,有對象放回時,使用return [[object retain]autorelease]。

我們結合下面的例子來說明,並總結出該問題的幾種解決方案

 

12.          @implementation FooClass

13.           

14.          - (void)setObject:(MyObject *)object;

15.           {

16.               // 這裏故意沒有使用 autorelease,以便說明問題

17.               [_object release];

18.               _object = [object retain];

19.           }

20.           

21.          - (id)object;

22.           {

23.               return _object;

24.           }

25.           

26.          - (void)dealloc;

27.           {

28.               [_object release];

29.               [super dealloc];

30.           }

31.           

32.          @end

33.           

34.          @implementation BarClass

35.           

36.          - (void)doStuff;

37.           {

38.              FooClass * foo = [[FooClass alloc] init];

39.           

40.              // 創建第一個對象,引用計數 = 1

41.              MyObject * firstObject = [[MyObject alloc] init];

42.              // setObject中由於 [object retain] ,引用計數 = 2

43.              [foo setObject:firstObject];

44.              // 釋放一次,引用計數 = 1;這之後對象有正確的所有權屬性

45.              [firstObject release];

46.           

47.              // 通過非 alloc/new/copy/mutableCopy 開頭函數得到對象

48.              // anObject 指向第一個對象,但是並沒有其所有權,對象引用計數 = 1

49.              MyObject * anObject = [foo object];

50.              [anObject testMethod];

51.           

52.              // 創建第二個對象

53.              MyObject * secondObject = [[MyObject alloc] init];

54.              // setObject中由於 [_object release]; 第一個對象引用計數 = 0,內存被釋放

55.              [foo setObject:secondObject];

56.              [secondObject release];

57.           

58.              // 程序在這裏崩潰了,因爲 anObject 指向了一個空地址

59.              [anObject testMethod];

60.          }

61.           

62.          @end

從結論我們來看看該問題的幾種可行的解決方案;各種方案中沒有列出的代碼與原先代碼一致。

生成對象時,使用autorelease

 

16.          @implementation BarClass

17.           

18.          - (void)doStuff;

19.           {

20.              FooClass * foo = [[FooClass alloc] init];

21.           

22.              MyObject * firstObject = [[[MyObject alloc] init] autorelease];

23.              [foo setObject:firstObject];

24.           

25.              MyObject * anObject = [foo object];

26.              [anObject testMethod];

27.           

28.              MyObject * secondObject = [[[MyObject alloc] init] autorelease];

29.              [foo setObject:secondObject];

30.           

31.              [anObject testMethod];

32.          }

33.           

34.          @end

對象生成時,即被放入最近的 pool 中,不需要人爲特殊的維護,對象的生命週期將被延續,出 {} 範圍之時即對象釋放之際。

對象代入時,先autorelease後再retain

 

•   - (void)setObject:(MyObject *)object;

•    {

•        [_object autorelease];

•        _object = [object retain];

•    }

•    

•   - (id)object;

•    {

•        // 遵循非 alloc/new/copy/mutableCopy 開頭的函數,不賜予所有權原則

•        return _object;

•    }

同樣的,對象被放入最近的 pool 中,第二次 setObject 後對象引用計數仍爲1, pool 清空時才執行最後一次對象release,從而保證了代碼的正確性。

對象在函數中返回時,使用return [[object retain] autorelease];

 

1. - (void)setObject:(MyObject *)object;

2.  {

3.      [_object release];

4.      _object = [object retain];

5.  }

6.  

7. - (id)object;

8.  {

9.      // 遵循非 alloc/new/copy/mutableCopy 開頭的函數,不賜予所有權原則

10.               return [[_object retain] autorelease];

11.           }

好不容易回到了本小節要說明的方法;可以看到這是從另一個角度解決了該問題:[foo object] 的時候保證引用計數是2,並將對象放入pool中維護。

總結上面3種方法,雖說是從不同角度入手解決了這個問題,但是基本原則不變,利用 NSAutoreleasePool 機制幫程序員維護代碼,管理內存。

如果你覺得3種編碼原則怎麼搭配使用,在什麼樣的場合下選擇比較麻煩,不要緊,都用就得了。我們犧牲的只是NSAutoreleasePool 中的一些內存,一小許性能損失罷了,這總比我們的程序崩潰了強。

 

 

四、ARC 誕生

ARC 是什麼我不需要再解釋,若有不明白,可以看看前面“ARC是什麼”這個章節

ARC 嚴格遵守 Objective-C 內存管理的基本原則

 

•  自己生成的對象,那麼即是其持有者。

•  不是自己生成的對象,也可成爲其持有者(一個對象可以被多個人持有)

•  如果不想持有對象的時候,必須釋放其所有權。

•  不能釋放已不再持有所有權的對象

並從編譯器角度維護了該原則,比如如果不是 alloc/new/copy/mutableCopy 開頭的函數,編譯器會將生成的對象自動放入 autoReleasePool 中。如果是 __strong 修飾的變量,編譯器會自動給其加上所有權。等等,詳細,我們根據不同的關鍵字來看看編譯器爲我們具體做了什麼。並從中總結出 ARC 的使用規則。

__strong

我們先來看看用 __strong 修飾的變量,以及缺省隱藏的 __strong 情況。

1. {

2.     /*

3.      * 生成對象並擁有所有權

4.      */

5.     id __strong obj = [[NSObject alloc] init];

6.  

7.     /*

8.      * 自己一直是持有對象狀態

9.      */

10.          }

11.              /*

12.               * 變量出生命週期時,失去全部所有者,對象內存空間被釋放

13.               */

這種情況毫無懸念,缺省使用 alloc/new/copy/mutableCopy 開頭的函數也是這樣的結果。並且在這裏,編譯器幫我們自動的調用了對象的 release 函數,不需要手工維護。再看看下面的情況。

1. {

2.     /*

3.      * 生成對象但是並沒有其所有權

4.      */

5.     id __strong obj = [NSMutableArray array];

6.  

7.     /*

8.      * 由於變量聲明是強引用,自己一直是持有對象狀態

9.      * 編譯器根據函數名,再將該對象放入 autoreleasepool 中

10.               */

11.          }

12.              /*

13.               * 變量出生命週期時,失去全部所有者,對象內存空間被釋放

14.               */

由上,雖然不是用 alloc/new/copy/mutableCopy 開頭的函數得到的對象,由於是強參照,我們仍然成爲對象的持有者。而這正是編譯器幫我們做到的。

具體做的是什麼呢?其實就是【對象在函數中返回時,使用return [[object retain] autorelease]】所描述的;如果你反彙編一下ARC生成的代碼,可以看到這時會自動調用名爲 objc_retainAutoreleaseReturnValue 的函數,而其作用和 [[object retain] autorelease] 一致。編譯器通過函數名分析,如果不是 alloc/new/copy/mutableCopy 開頭的函數,自動加入了這段代碼。

另外,缺省 __strong 修飾的變量,對象代入的時候也正確地保證對象所有者規則;代入新對象時,自動釋放舊對象的參照,代入nil的時候,表示釋放當前對象的強參照。

__weak

雖然大部分場合,大部分問題使用 __strong 來編碼就足夠了;但是爲了解決循環參照的問題 __weak 關鍵字修飾【弱參照】變量就發揮了作用。關於循環參照的問題,準備在以後的博文中介紹;今天,主要看看編譯器在背後怎麼處理 __weak 變量的。

__weak 聲明的變量其實是被放入一個weak表中,該表和引用計數的表格類似,是一個Hash表,都是以對象的內存地址做key,同時,針對一個對象地址的key,可以同時對應多個變量的地址。

當一個 __weak 所指對象被釋放時,系統按下面步驟來處理

 

•  從weak表中,通過對象地址(key)找到entry

•  將entry中所有指向該對象的變量設爲nil

•  從weak表中刪除該entry

•  從對象引用計數表中刪除對象entry(通過對象地址找到)

另外,當使用 __weak 修飾的變量的時候,變量將放入 autoreleasepool 中,並且用幾次放幾次。比如下面的簡單例子。

1. {

2.     id __weak o = obj;

3.     NSLog(@"1 %@", o);

4.     NSLog(@"2 %@", o);

5.     NSLog(@"3 %@", o);

6.     NSLog(@"4 %@", o);

7.     NSLog(@"5 %@", o);

8. }

這裏我們用了5次,那麼pool中就被登記了5次;從效率上考慮這樣當然不是很好,可以通過代入 __strong 修飾的強參照變量來避開這個問題。

1. {

2.     id __weak o = obj;

3.     id __strong temp = o;

4.     NSLog(@"1 %@", temp);

5.     NSLog(@"2 %@", temp);

6.     NSLog(@"3 %@", temp);

7.     NSLog(@"4 %@", temp);

8.     NSLog(@"5 %@", temp);

9. }

另外,還有通過重載 allowsWeakReference和retainWeakReference 函數來限制 __weak 聲明變量使用回數的方法,畢竟不在本次討論範疇之內,就此省略。

話說回來,爲什麼使用弱參照變量的時候,要將其放入 autoreleasepool 中呢?想想弱參照的定義就應該明白了 —- 如果在訪問弱參照對象時,該對象被釋放了怎麼辦,程序不就崩潰了;所以爲了解決該問題,又再一次用到了 pool。

__autoreleasing

雖然上面還沒有講到該關鍵字,但是編譯器在很多時候已經用到了 autoreleasepool。比如非alloc/new/copy/mutableCopy 開頭的函數返回一個對象的時候,又比如使用一個 __weak 聲明的變量的時候。

實際上,寫ARC代碼的時候,明示 __autoreleasing 聲明變量和明示 __strong 聲明變量一樣基本上沒有,因爲編譯器已經爲我們做了很多,很智能了(前提是我們要按ARC的規則寫代碼)。

還有一種編譯器缺省使用 __autoreleasing 關鍵字聲明變量的時候:對象指針類型。比如下面的對應關係。

1.    id *obj == id __autoreleasing *obj

2.    NSObject **obj == NSObject * __autoreleasing *obj

所以,下面兩個函數的是等價的。

1. -(BOOL)performOperationWithError:(NSError **)error;

2.  

3. -(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;

像下面的函數調用,爲什麼是可行的呢?

1. NSError __strong *error = nil;

2. BOOL result = [obj performOperationWithError:&error];

其實,編譯器是這樣解釋這段代碼的。

1. NSError __strong *error = nil;

2. NSError __autoreleasing *tmp = error;

3. BOOL result = [obj performOperationWithError:&tmp];

4. error = tmp;

那麼我們這樣聲明函數不就可以了嗎?

1. -(BOOL)performOperationWithError:(NSError * __strong *)error;

答案是肯定的,你可以這樣做,編譯是可以通過,但你違反了非 alloc/new/copy/mutableCopy 開頭的函數,不返回對象持有權的原則。這裏是沒有問題了,但也許影響到其他地方NG。

ARC 規則

結合上面的講解,我想你也應該能夠總結出來使用ARC的規則

 

(這裏只列出本講中涉及的內容,其他的內容以後總結)

•  代碼中不能使用retain, release, retain, autorelease

•  不能使用NSAllocateObject, NSDeallocateObject

•  不能使用NSAutoReleasePool、而需要@autoreleasepool塊

•  嚴守內存管理相關函數命名規則

關於函數命名,伴隨ARC的導入,還有一系列函數的定義也被嚴格定義了,那就是以 init 開頭的函數。init 函數作爲alloc生成對象的初期化函數,需要按原樣直接傳遞對象給調用段,所以下面的聲明是OK的。

1. -(id)initWithObject:(id)obj;

而下面的是NG的

1. -(void)initWithObject;

不過聲明爲 -(void) initialize; 是沒有問題的。

 

 

 

 

 

 

 

概念

當我們使用強參照(Strong reference)時,往往需要留意 循環參照 的問題。循環參照指的是兩個對象被互相強參照,以至於任一對象都不能釋放。

一般情況下,當對象之間有“父子關係”時,強參照的情況發生的比較多。比如通訊薄對象AddrBook和每個通訊錄Entry的關係如下。

 

這種情況下,由於Entry對象被AddrBook強參照,所以不能釋放。另一方面,如果Entry被釋放了,AddrBook對象的強參照也就沒有了,其對象也應被釋放。

解決方式

像上面的例子,當多個對象間有“父子關係”時,需要在一側用“弱參照”來解決循環參照問題。一般情況下,“父親”作爲“孩子”的擁有者,對“孩子”是強參照,而“孩子”對父親是弱參照。


如圖所示,當強參照AddrBook對象的變量被釋放的時候,AddrBook對象將被自動釋放,同時將失去Entry成員對象的強參照。另外,當AddrBook對象被釋放的時候,Entry對象中的AddrBook變量也將由Zeroing機制,自動帶入nil。我們不需要擔心釋放對象的再訪問問題。

下面,我們將看看有幾種情況下,需要注意循環參照問題。

Delegate模式

iOS程序中經常用到delegate模式,比如ViewController中,用ModalView打開/關閉DetailViewController時,需要delegate的設定。

 

這裏,ViewController對象中強參照detailViewController,如果DetailViewController的delegate不是弱參照ViewController話,將引起循環參照。

另外,當類中使用weak @property聲明的delegate變量時,如果參照對象被釋放,該變量將被自動設爲nil,不需要程序代碼設置。

Blocks

Blocks是iOS 4開始導入的,可以理解爲python或者lisp中的Lambda,C++11也已導入了該概念;類似概念ruby/smalltalk/JSP語言中也有定義。具體講解見以後的文章,本節我們主要看看在Block中的循環參照問題。

比如,block對象用copy的屬性定義時候

22.          typedef void(^MyBlock)(void);

23.           

24.          @interface MyObject : NSObject

25.          @property (nonatomic, copy) MyBlock block;

26.          @property (nonatomic, strong) NSString *str;

27.           

28.          - (void)performBlock;

29.          @end

30.           

31.          @implementation MyObject

32.          @synthesize block, str;

33.           

34.          - (void)performBlock {

35.              if (self.block) {

36.                  self.block();

37.              }

38.          }

39.          @end

調用端如下:

28.          MyObject *object = [[MyObject alloc] init];

29.          object.str = @"hoge";

30.           

31.          object.block = ^{

32.              NSLog(@"block: str=%@", object.str);

33.          };

34.          [object performBlock];

我們看到,Block的構文中參照了object,同樣object也強參照block


爲了解決該問題,我們可以有下面兩種選擇。

使用__block關鍵字修飾

使用__block關鍵字,讓對象有讀寫權限,如果Block內的處理完畢就釋放object

•   __block MyObject *object = [[MyObject alloc] init];

•   object.str = @"hoge";

•    

•   object.block = ^{

•       NSLog(@"block: str=%@", object.str);

•       object = nil;

•   };

•   [object performBlock];

該關鍵字的意思就是讓block取消對object的強參照,以避免循環參照。但是,有一個問題就是,object的釋放動作是在Block內部執行,如果Block沒有被執行的話,循環參照一直存在。比如上面的代碼,如果第8行 [object performBlock]; 沒有執行的話,那麼一直還是循環參照狀態。

使用__weak關鍵字修飾

另一種方案就是讓Block的參照變爲弱參照

•   MyObject *object = [[MyObject alloc] init];

•   object.str = @"hoge";

•    

•   __weak MyObject *weakObject = object;

•   object.block = ^{

•       NSLog(@"block: str=%@", weakObject.str);

•   };

•   [object performBlock];

考慮到異步通信時Blocks的使用情況,weak變量weakObject有可能隨時變爲nil,所以類似於下面先變爲strong變量,並檢查是否爲nil的處理方式應該更安全。

•   MyObject *object = [[MyObject alloc] init];

•   object.str = @"hoge";

•    

•   __weak MyObject *weakObject = object;

•   object.block = ^{

•       MyObject strongObject = weakObject;

•       if (strongObject) {

•           NSLog(@"block: str=%@", strongObject.str);

•       }

•   };

•   [object performBlock];

總上,當我們使用Blocks時,也需要考慮Block中變量和實例的關係,不要引起不必要的循環參照問題

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章