ARC

一、ARC

ARC 的想法來源于蘋果在早期設(shè)計 Xcode 的 Analyzer 的時候,發(fā)現(xiàn)編譯器在編譯時可以幫助大家發(fā)現(xiàn)很多內(nèi)存管理中的問題。后來蘋果修改了一些內(nèi)存管理代碼的書寫方式,干脆編譯器在編譯時把內(nèi)存管理的代碼都自動補上。

ARC 是編譯器特性,而不是運行時特性,更不是垃圾回收器(GC)。

Automatic Reference Counting (ARC) is a compiler-level feature that simplifies the process of managing object lifetimes (memory management) in Cocoa applications.

程序在編譯的時候,編譯器會分析源碼中每個對象的生命周期,然后基于這些對象的生命周期,編譯器幫我們在合適的地方插入retain、release 等代碼以管理對象的引用計數(shù),從而達到自動管理對象生命周期的目的。

所以 ARC 是工作在編譯期的一種技術(shù)方案,這樣的好處:

  1. 編譯之后,ARC 與 MRC 代碼是沒有什么差別的,所以二者可以在源碼中共存

  2. 相對于垃圾回收這類內(nèi)存管理方案,ARC 不會帶來運行時的額外開銷,所以對于應(yīng)用的運行效率不會有影響。相反的,由于ARC 能夠深度分析每一個對象的生命周期,它能夠做到比人工管理引用計數(shù)更加高效。例如在一個函數(shù)中,對一個對象剛開始有一個引用計數(shù) +1 的操作,之后又緊接著有一個 -1 的操作,那么編譯器就可以把這兩個操作都優(yōu)化掉。

只有編譯器是無法單獨完成這一工作的,還需要 OC 運行時庫的配合協(xié)助,因此 ARC 的實現(xiàn)工具主要包括:

  1. LLVM 編譯器(clang 3.0 以上)
  2. OC 運行時庫 493.9 以上

weak 變量能夠在引用計數(shù)為 0 時被自動設(shè)置成 nil,顯然是有運行時邏輯在工作的。

ARC 能夠解決 iOS 開發(fā)中 90% 的內(nèi)存管理問題,但是另外 10% 的內(nèi)存管理問題是需要開發(fā)者處理的,這主要是與底層 Core Foundation 對象交互的部分,底層 Core Foundation 對象由于不在 ARC 的管理下,所以需要自己維護這些對象的引用計數(shù)。

二、ARC 的開啟和關(guān)閉

在 Targets -》Build Settings 中搜索 Automatic Reference Counting,可以修改它的布爾值,yes - 開啟 no - 關(guān)閉。

arc

如果需要對特定文件開啟或關(guān)閉 ARC,可以在 Targets -》Build Phases -》Compile Sources,在里面找到對應(yīng)文件,添加flag:

開啟:-fobjc-arc 關(guān)閉:-fno-objc-arc

arc

三、ARC 的修飾符

主要提供了 4 種修飾符,他們分別是:__strong、__weak、__autoreleasing、__unsafe_unretained。

3.1 __strong

強引用。相當于 @property 的 "strong"。所有對象只有當沒有任何一個強引用指向(引用計數(shù)為 0)時,才會被釋放。

注意:如果在聲明引用時不加修飾符,那么將默認是強引用。當需要釋放強引用指向的對象時,需要將強引用置 nil。

使用 __strong 修飾變量的程序運行過程。

{
    id __strong object = [[NSObject alloc] init];
}

轉(zhuǎn)換后的模擬源代碼為:

/*編譯器的模擬代碼*/
id object = objc_msgSend(NSObjct, @selector(alloc));
objc_msgSend(object, @selector(init));
objc_release(object);

對象變量生成時,分別調(diào)用 alloc 和 init 方法,對象變量作用域結(jié)束時調(diào)用 objc_release 方法釋放對象變量,雖然 ARC 情況下不能使用 release 方法,但是由此可見編譯器編譯時在合適的地方插入了 release。

在使用 alloc、new、copy、mutableCopy 以外的方法生成對象變量方法時會有什么不同

{
    id __strong object = [NSMutableArray array];
}

調(diào)用 array 的類方法轉(zhuǎn)換后:

{
    /*編譯器的模擬代碼*/
    id object = objc_msgSend(NSMutableArray, @selector(array));
    objc_retainAutoreleasedReturnValue(object);
    objc_release(object);
}

objc_retainAutoreleasedReturnValue(object) 函數(shù)的作用:最優(yōu)化程序運行。

自己持有(retain)對象的函數(shù),但它持有的應(yīng)為返回注冊在 autoreleasepool 中對象的方法或函數(shù)的返回值。

objc_retainAutoreleasedReturnValue 函數(shù)與 objc_autoreleasedReturnValue 是成對出現(xiàn)的,現(xiàn)在看看 NSMutableArray 類的 array 類方法的編譯器實現(xiàn)。

+ (id)array {
    return [[NSMutableArray alloc] init];
}

轉(zhuǎn)換后的源代碼。

+ (id)array
{
    /*編譯器的模擬代碼*/
    id obj = objc_msgSend(NSMutableArray, @selector(alloc));
    objc_msgSend(obj, @selector(init));
    return objc_autoreleaseReturnValue(obj);
}

通過 objc_autoreleaseReturnValue 函數(shù)將對象注冊在自動釋放池 autoreleasepool 中并返回,但是與 objc_autorelease 函數(shù)不同的是,objc_autoreleaseReturnValue 函數(shù)一般不僅限于注冊對象到 autoreleasepool 中去。

objc_autoreleaseReturnValue 與 objc_retainAutoreleasedReturnValue 的配合使用,可以不將對象注冊到autoreleasepool 中而直接傳遞,達到最優(yōu)化。

objc_autoreleaseReturnValue 函數(shù)會檢查使用該函數(shù)的方法或者函數(shù)的調(diào)用方的執(zhí)行命令列表,如果調(diào)用方在調(diào)用該函數(shù)或方法之后,緊接著調(diào)用了 objc_retainAutoreleasedReturnValue 函數(shù),那么不再將對象注冊到 autoreleasepool 中去,而直接將對象傳遞給調(diào)用方。

相比于 objc_retain 函數(shù)來說 objc_retainAutoreleasedReturnValue 函數(shù)在返回一個即使沒有注冊到 autoreleasepool 中的對象,也能正確的獲取對象。

3.2 __weak

弱引用。相當于 @property 的 "weak"。弱引用不會影響對象的引用計數(shù),即只要對象沒有任何強引用指向,即使有 n 個弱引用對象指向也沒用,該對象依然會被釋放。

對象在被釋放的同時,指向它的弱引用(weak)會自動被置 nil,這個技術(shù)叫 zeroing weak pointer。這樣有效的防止無效指針、野指針的產(chǎn)生。__weak 一般用在 delegate 關(guān)系中防止循環(huán)引用或者用來修飾指向由 Interface Builder 編輯與生成的 UI 控件。

{
    id _weak object = [[NSObject alloc] init];
}

轉(zhuǎn)換后的模擬源代碼。

{
    /* 編譯器的模擬代碼 */
    id object;
    id tmp = objc_msgSend(NSObject, @selector(alloc));
    objc_msgSend(tmp, @selector(init));
    objc_initWeak(&object, tmp);
    objc_release(tmp);
    objc_destoryWeak(&object);
}

自己生成并且持有的對象通過 objc_initWeak 函數(shù)賦值給 __weak 修飾符的變量,但是編譯器判斷并沒有對其進行持有,因此該對象通過 objc_release 函數(shù)被釋放和廢棄。

隨后通過 objc_destoryWeak 將引用廢棄對象的附有 __weak 修飾符的變量置為 nil。

如果不是直接賦值,而是通過使用 __weak 修飾符來引用變量時。

{
    id __weak object = obj;
    NSLog(@"%@", object);
}

轉(zhuǎn)換后的模擬源代碼。

/*編譯器的模擬代碼*/
{
    id object;
    objc_initWeak(&object, obj);
    id temp = objc_loadWeakRetained(&object);
    objc_autorelease(temp);
    NSLog(@"%@", temp);
    objc_destoryWeak(&object);
}

明顯增加了 objc_loadWeakRetained 與 objc_autorelease 函數(shù)調(diào)用,他們的主要作用是:

  1. objc_loadWeakRetained 函數(shù)取出 __weak 修飾符變量引用的對象并且 retain
  2. objc_autorelease 函數(shù)將引用的對象注冊到 autoreleasepool 中。

因此,使用 __weak 修飾符引用的對象都被注冊到 autoreleasepool 中,在 @autoreleasepool 塊結(jié)束之前都可以放心使用,大量使用 __weak 修飾符的變量,導致注冊到 autoreleasepool 中的對象也大量地增加。所以在使用 __weak 修飾符引用的變量時,最好先暫時用 __strong 修飾符的變量進行引用后再使用。

2 種不能使用 __weak 修飾符的情況:

  • 重寫了 retain/release 的類,例如 NSMachPort 類;
  • 當 allowsWeakReference/retainWeakReference 實例方法返回 NO 時。

3.3 __autoreleasing

對象被加入到 autorelease pool,是會自動釋放的引用,與 MRC 中 autorelease 的用法相同。定義 @property 時不能使用這個修飾符。

對于 alloc、new、copy、mutableCopy 的實現(xiàn)。

@autoreleasepool{
    id __autoreleasing object = [[NSObject alloc] init];
}

轉(zhuǎn)換后的模擬源代碼。

{
    /* 編譯器的模擬代碼 */
    id pool = objc_autoreleasePoolPush();
    id object = objc_msgSend(NSObjct, @selector(alloc));
    objc_msgSend(object, @selector(init));
    // 調(diào)用autorelease方法
    objc_autorelease(object);
    id pool = objc_autoreleasePoolPop();
}

NSMutableArray 類中的 array 方法如何實現(xiàn) autorelease 功能。

@autoreleasepool{
    id __autoreleasing object = [NSMutableArray array];
}

轉(zhuǎn)化后的模擬源代碼。

{
    /* 編譯器的模擬代碼 */
    id pool = objc_autoreleasePoolPush();
    id object = objc_msgSend(NSMutableArray, @selector(array));
    objc_retainAutoreleasedReturnValue(object);
    // 調(diào)用 autorelease 方法
    objc_autorelease(object);
    id pool = objc_autoreleasePoolPop();
}

除了持有對象的方法從 alloc 變成了 objc_retainAutoreleasedReturnValue 函數(shù),但是注冊到 autoreleasepool 的方法沒有變化,都是調(diào)用了 objc_autorelease 函數(shù)。

一個常見的誤解是,在 ARC 中沒有 autorelease,因為這樣一個“自動釋放”看起來好像有點多余。

這個誤解可能源自于將 ARC 的“自動” 和 autorelease “自動” 的混淆。其實你只要看一下每個 iOS App 的 main.m 文件就能知道,autorelease 不僅好好的存在著,并且不需要再手工被創(chuàng)建,也不需要再顯式得調(diào)用 [pool drain] 方法釋放內(nèi)存池。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

以下兩行代碼的意義是相同的。

NSString * str = [[[NSString alloc] initWithFormat:@"China"] autorelease];   // MRC
NSString * __autoreleasing str = [[NSString alloc] initWithFormat:@"China"]; // ARC

__autoreleasing 在 ARC 中主要用在參數(shù)傳遞返回值(out-parameters)和引用傳遞參數(shù)(pass-by-reference)的情況下。

__autoreleasing is used to denote arguments that are passed by reference (id *) and are autoreleased on return.

比如常見的 NSError 的使用:

NSError * __autoreleasing error;
 
// writeToFile方法中 error 參數(shù)的類型為 (NSError *__autoreleasing *))
if (![data writeToFile:filename options:NSDataWritingAtomic error:&error])  { 
  NSLog(@"Error: %@", error.localizedDescription); 
}

注意:如果 error 的修飾符為 strong,那么,編譯器會幫你隱式地做如下事情,保證最終傳入函數(shù)的參數(shù)依然是個 __autoreleasing 類型的引用。

NSError * error; 
NSError * __autoreleasing tempError = error;  // 編譯器添加
 
if (![data writeToFile:filename options:NSDataWritingAtomic error:&tempError]) 
{ 
  error = tempError; // 編譯器添加 
  NSLog(@"Error: %@", error.localizedDescription); 
}

為了避免這種情況,提高效率,一般在定義 error 的時候?qū)⑵渎暶鳛開_autoreleasing 類型的:

NSError *__autoreleasing error;

加上 __autoreleasing 之后,相當于在 MRC 中對返回值 error 做了如下事情:

*error = [[[NSError alloc] init] autorelease];

*error 指向的對象在創(chuàng)建出來后,被放入到了 autoreleasing pool 中,等待使用結(jié)束后的自動釋放,函數(shù)外 error 的使用者并不需要關(guān)心 *error 指向?qū)ο蟮尼尫拧?/p>

另外,在 ARC 中,所有這種指針的指針(NSError **)的函數(shù)參數(shù)如果不加修飾符,編譯器會默認將他們認定為 __autoreleasing 類型。

比如下面的兩段代碼是等同的:

- (NSString *)doSomething:(NSNumber **)value
{
     // do something  
}
- (NSString *)doSomething:(NSNumber * __autoreleasing *)value
{ 
     // do something 
}

除非顯式得給 value 聲明了 __strong,否則 value 默認就是 __autoreleasing 的。

最后一點,某些類的方法會隱式地使用自己的 autorelease pool,在這種時候使用 __autoreleasing 類型要特別小心。

比如 NSDictionary 的 - enumerateKeysAndObjectsUsingBlock: 方法會隱式地創(chuàng)建一個 autorelease pool.

- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error
{
    [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop){
          // do stuff  
          if (...)  {
                *error = [NSError errorWithDomain:@"Not Found" code:404 userInfo:nil];
          }
    }];
}

上面代碼實際類似于:

- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error
{
    [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop){
          @autoreleasepool  // 被隱式創(chuàng)建
      {
              if (...) {
                    *error = [NSError errorWithDomain:@"Not Found" code:404 userInfo:nil];
              }
          }
    }];
    // *error 在這里已經(jīng)被dict的做枚舉遍歷時創(chuàng)建的 autorelease pool 釋放掉了 
} 

為了能夠正常的使用 *error,我們需要一個 strong 型的臨時引用,在 dict 的枚舉 block 中使用這個臨時引用,保證引用指向的對象不會在出了 dict 的枚舉 block 后被釋放,正確的方式如下:

- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error
{
  __block NSError * tempError;  // 加 __block 保證可以在 Block 內(nèi)被修改  
  
   [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { 
    
      if (...)  { 
      *tempError = [NSError errorWithDomain:@"Not Found" code:404 userInfo:nil]; 
    }  
  }] 
  if (error != nil) 
  { 
    *error = tempError; 
  } 
} 

3.4 __unsafe_unretained

ARC 是在 iOS 5 引入的,而這個修飾符主要是為了在 ARC 剛發(fā)布時兼容 iOS 4 以及版本更低的設(shè)備,因為這些低版本的設(shè)備沒有 weak pointer system,這個系統(tǒng)簡單的理解就是上面講 weak 時提到的,能夠在 weak 引用指向?qū)ο蟊会尫藕?,把引用值自動設(shè)為 nil。

相當于 @property 的 "unsafe_unretained",實際可以將它理解為 MRC 時代的 assign:純粹只是將引用指向?qū)ο?,沒有任何額外的操作,在指向?qū)ο蟊会尫艜r依然指向原來被釋放的對象(所在的內(nèi)存區(qū)域)。所以非常不安全。

現(xiàn)在可以完全忽略掉這個修飾符了,因為 iOS 4 早已退出歷史舞臺很多年。

{
    id __unsafe_unretained object = [[NSObject alloc] init];
}

轉(zhuǎn)換后的模擬源代碼。

{
     /*編譯器的模擬代碼*/
    id object = objc_msgSend(NSObject, @selector(alloc));
    objc_msgSend(object, @selector(init));
    objc_release(tmp);
}

可見通過 __unsafe_unretained 修飾的變量引用了對象但是并不持有對象,對象在釋放和廢棄后,并沒有調(diào)用被 __unsafe_unretained 修飾的變量的 objc_destoryWeak 函數(shù),因此該對象的懸垂指針被賦值給變量 object,導致引用變量 object 時發(fā)生崩潰。

3.5 正確使用修飾符

蘋果的文檔中明確地寫道:

You should decorate variables correctly. When using qualifiers in an object variable declaration,
the correct format is:
??ClassName * qualifier variableName;

按照這個說明,要定義一個 weak 修飾的 NSString 引用,它的寫法應(yīng)該是:

NSString * __weak str = @"Hello";   // 正確

而不應(yīng)該是:

__weak NSString *str = @"Hello";   // 錯誤

那這里就有疑問了,既然文檔說是錯誤的,為啥編譯器不報錯呢?文檔又解釋道:

Other variants are technically incorrect but are "forgiven" by the compiler. To understand the issue, see http://cdecl.org/.

看來是蘋果考慮到很多人會用錯,所以在編譯器這邊貼心地幫我們忽略并處理掉了這個錯誤。雖然不報錯,但是我們還是應(yīng)該按照正確的方式去使用這些修飾符。

3.6 棧中指針默認值為 nil

無論是被 strong、weak 還是 autoreleasing 修飾,聲明在棧中的指針默認值都會是 nil。所有這類型的指針不用再初始化的時候置 nil 了。這個特性更加降低了“野指針”出現(xiàn)的可能性。

在 ARC 中,以下代碼會輸出 null 而不是 crash。

- (void)myMethod 
{
    NSString * name;
    NSLog(@"%@", name);
}

四、ARC 與 Block

在手動管理內(nèi)存時代,block 會隱式地對進入其作用域內(nèi)的對象(或者說被 block 捕獲的指針指向的對象)執(zhí)行 retain 操作,來確保 block 使用到該對象時,能夠正確的訪問。

MyViewController * myController = [[MyViewController alloc] init…]; 

myController.dismissBlock =  ^(NSString * result) {
    // 隱式地調(diào)用 [myController retain]; 造成循環(huán)引用
    [myController dismissViewControllerAnimated:YES completion:nil];
};

[self presentViewController:myController animated:YES completion:^{
    // 調(diào)用[myController release];是在 MRC 中的一個常規(guī)寫法,并不能解決上面循環(huán)引用的問題
    [myController release]; 
}];

@interface SecondVC : UIViewController
@property (nonatomic, copy) void (^ block)(void);
@end
{
    SecondVC * vc = [[SecondVC alloc] init];
    
    NSLog(@"%lu", (unsigned long)vc.retainCount);
    
    vc.block = ^ {
        NSLog(@"%lu", (unsigned long)vc.retainCount);
    };
    vc.block();
}
2018-11-16 10:26:05.872092+0800 Demo[49289:1083433] 1
2018-11-16 10:26:05.872214+0800 Demo[49289:1083433] 2

dismissBlock 調(diào)用了 [myController dismiss..] 方法,這時 dismissBlock 會對 myController 執(zhí)行 retain 操作。

而作為 myController 的屬性,myController 對 dismissBlock 也至少有一個 retain(一般準確講是 copy),這時就出現(xiàn)了在內(nèi)存管理中最糟糕的情況:循環(huán)引用。也就是說:相互持有對方。循環(huán)引用導致了 myController 和 dismissBlock 最終都不能被釋放。

對 delegate 指針用 weak 就是為了避免這種問題。

不過好在,編譯器會及時地給我們一個警告,提醒我們可能會發(fā)生這類型的問題:

arc

我們一般用如下方法解決:給進入 block 的指針加一個 __block 修飾符。

這個 __block 在 MRC 時代有兩個作用:

  • 說明變量可改
  • 說明指針指向的對象不做隱式的 retain 操作

除了靜態(tài)變量和全局變量不需要加 __block 就可以在 block 中修改外,其他變量不加則不能在 block 中修改。

對代碼做出修改,解決了循環(huán)引用的問題:

MyViewController * __block myController = [[MyViewController alloc] init…]; 
myController.dismissBlock =  ^(NSString * result) {
    [myController dismissViewControllerAnimated:YES completion:nil];
}; 
// 之后正常的 release 或者 retain

在 ARC 環(huán)境下,沒有了 retain 和 release 等操作,情況也發(fā)生了改變:

在任何情況下,__block 修飾符的作用只有上面的第一條:說明變量可改。即使加上了 __block 修飾符,一個被 block 捕獲的強引用也依然是一個強引用。

所以在 ARC 下,如果還按照 MRC 下的寫法,添加 __block 是沒有解決循環(huán)引用的問題。

代碼修改如下:

__block MyViewController * myController = [[MyViewController alloc] init…]; 
myController.dismissBlock =  ^(NSString * result) {
    [myController dismissViewControllerAnimated:YES completion:nil];
    myController = nil;  // 注意這里,保證了 block 結(jié)束對 myController 強引用的解除
};

在 block 中將捕獲的指針置為 nil,保證了 dismissBlock 對 myController 強引用的解除,不過也同時解除了myController 指針對 myController 對象的強引用。

更好的方法就是使用 weak。(或者為了考慮 iOS4 的兼容性用 unsafe_unretained,具體用法和 weak 相同)

為了保證 dismissBlock 對 myController 沒有強引用,我們可以定義一個臨時的弱引用 weakMyViewController 來指向原myController 的對象,并把這個弱引用傳入到 dismissBlock 內(nèi),這樣就保證了 dismissBlock 對 myController 持有的是一個弱引用,而不是一個強引用。如此,繼續(xù)修改代碼如下:

MyViewController * __weak weakMyViewController = myController;
myController.dismissBlock =  ^(NSString * result) {
    [weakMyViewController dismissViewControllerAnimated:YES completion:nil];
};

這樣循環(huán)引用的問題就解決了,但是卻引入了一個新的問題:由于傳入 dismissBlock 的是一個弱引用,那么當 myController指向的對象在 dismissBlock 被調(diào)用前釋放,那么 dismissBlock 就不能正常的運作了。在一般的單線程環(huán)境中,這種問題出現(xiàn)的可能性不大,但是到了多線程環(huán)境,就很不好說了,所以我們需要繼續(xù)完善這個方法。

為了保證在 dismissBlock 內(nèi)能夠訪問到正確的 myController,我們在 dismissBlock 內(nèi)新定義一個強引用strongMyController 來指向 weakMyController 指向的對象,這樣多了一個強引用,就能保證這個 myController 對象不會在 dismissBlock 被調(diào)用前釋放掉了。于是,對代碼再次做出修改:

MyViewController * __weak weakMyController = myController;
// __weak typeof(myController) weakMyController = myController;
myController.dismissBlock =  ^(NSString * result) {
    MyViewController * strongMyController = weakMyController;
    // __strong typeof(weakMyController) strongMyController = weakMyController;
    if (strongMyController) {
         [strongMyController dismissViewControllerAnimated:YES completion:nil];
    }
    else {
    }
};

很多讀者會有疑問,不是不希望 block 對原 myController 對象增加強引用么,這里為什么堂而皇之地在 block 內(nèi)新定義了一個強引用,這個強引用不會造成循環(huán)引用么?

理解這個問題的關(guān)鍵在于被 block 捕獲的引用和在 block 內(nèi)定義的引用的區(qū)別。為了搞得明白這個問題,這里需要了解一些Block 的實現(xiàn)原理,詳細的內(nèi)容可以參考其他的文章:談Objective-C block的實現(xiàn)、block 實現(xiàn)、正確使用Block避免Cycle Retain和Crash。

為了更清楚地說明問題,這里用一個簡單的程序舉例。如下程序:

#include <stdio.h>
int main()
{
    int b = 10;
    int *a = &b;
    
    void (^ block)() = ^() {
         int *c = a;
    };
    block(); 
    return 1;
}

程序中,同為 int 型的指針,a 變量被 block 捕獲,而 c 變量是在 block 內(nèi)定義的。用 clang -rewrite-objc 命令處理后,可以看到如下代碼。

原 main 函數(shù):

int main()
{
    int b = 10;
    int *a = &b;
   
    void (*block)() = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
    return 1;
}

block 的結(jié)構(gòu):

struct __main_block_impl_0 { 
    struct __block_impl impl;
    struct __main_block_desc_0* Desc; 
    
    int *a;  // 被捕獲的引用 a 出現(xiàn)在了 block 的結(jié)構(gòu)體里面
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

實際執(zhí)行的函數(shù):

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
     int *a = __cself->a; // bound by copy
     int *c = a; // 在 block 中聲明的引用 c 在函數(shù)中聲明,存在于函數(shù)棧上
}

可以清楚的看到,a 和 c 存在的位置完全不同,如果 block 存在于堆上(在 ARC 下 block 默認在堆上),那么 a 作為 block 結(jié)構(gòu)體的一個成員,也自然會存在于堆上,而 c 無論如何,永遠位于 block 內(nèi)實際執(zhí)行代碼的函數(shù)棧內(nèi)。這也導致了兩個變量生命周期的完全不同:c 在 block 的函數(shù)運行完畢,即會被釋放,而 a 只有在 block 被從堆上釋放的時候才會釋放。

回到之前的示例,如果直接讓 dismissBlock 捕獲 myController 引用,那么這個引用會被復制后作為 dismissBlock 的成員變量存在于其所在的堆空間中,也就是為 dismissBlock 增加了一個指向 myController 對象的強引用,這就是造成循環(huán)引用的本質(zhì)原因。

對于 MyViewController 的例子,dismissBlock 的結(jié)構(gòu)體大概是這個樣子:

struct __main_block_impl_0 {
    
    struct __block_impl impl; 
    struct __main_block_desc_0* Desc;
    MyViewController * __strong myController;  // 被捕獲的強引用 myController
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
         impl.isa = &_NSConcreteStackBlock;
         impl.Flags = flags;
         impl.FuncPtr = fp;
         Desc = desc;
    }
};

而給 dismissBlock 傳入一個弱引用 weakMyController,這時 dismissBlock 的結(jié)構(gòu):

struct __main_block_impl_0 {
    struct __block_impl impl; 
    struct __main_block_desc_0* Desc;
    MyViewController * __weak weakMyController;  // 被捕獲的弱引用 weakMyController
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

在 dismissBlock 內(nèi)聲明的強引用 strongMyController,它雖然是強引用,但存在于函數(shù)棧中,在函數(shù)執(zhí)行期間,它一直存在,一直持有 myController 對象,但當函數(shù)執(zhí)行完畢,strongMyController 即被銷毀,于是它對 myController 對象的強引用被解除,這時 dismissBlock 對 myController 對象就不存在強引用關(guān)系了!

加入了 strongMyController 的函數(shù)大體會是這個樣子:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
     MyViewController * __strong strongMyController = __cself->weakMyController;
}

在 ARC 中,block 捕獲的引用和 block 內(nèi)聲明的引用,存儲空間與生命周期都是不同的。

實際上,在自動引用計數(shù)環(huán)境下,對 block 捕獲對象的內(nèi)存管理已經(jīng)簡化了很多,由于沒有了 retain 和 release 等操作,實際只需要考慮循環(huán)引用的問題就行了。

五、ARC 與 Toll-Free Bridging

There are a number of data types in the Core Foundation framework and the Foundation framework that can be used interchangeably. This capability, called toll-free bridging, means that you can use the same data type as the parameter to a Core Foundation function call or as the receiver of an Objective-C message.

Toll-Free Briding 保證了在程序中,可以方便和諧的使用 Core Foundation 類型的對象和 Objective-C 類型的對象。詳細的內(nèi)容可參考官方文檔。以下是官方文檔中給出的示例:

NSLocale * gbNSLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"];
CFLocaleRef gbCFLocale = (CFLocaleRef) gbNSLocale;
CFStringRef cfIdentifier = CFLocaleGetIdentifier (gbCFLocale);
NSLog(@"cfIdentifier: %@", (NSString *)cfIdentifier); // logs: "cfIdentifier: en_GB"
CFRelease((CFLocaleRef) gbNSLocale);
CFLocaleRef myCFLocale = CFLocaleCopyCurrent();
NSLocale * myNSLocale = (NSLocale *) myCFLocale;
[myNSLocale autorelease];
NSString * nsIdentifier = [myNSLocale localeIdentifier];
CFShow((CFStringRef) [@"nsIdentifier: " stringByAppendingString:nsIdentifier]); // logs identifier for current locale

在 MRC 時代,由于 Objective-C 類型的對象和 Core Foundation 類型的對象都是相同的 retain 和 release 操作規(guī)則,所以Toll-Free Bridging 的使用比較簡單,但是自從 ARC 加入后,Objective-C 類型的對象內(nèi)存管理規(guī)則改變了,而 Core Foundation 依然是之前的機制,換句話說,Core Foundation 不支持 ARC。

這個時候就必須要考慮一個問題,在做 Core Foundation 與 Objective-C 類型轉(zhuǎn)換的時候,用哪一種規(guī)則來管理對象的內(nèi)存。顯然,對于同一個對象,我們不能夠同時用兩種規(guī)則來管理,所以這里就必須要確定一件事情:哪些對象用 Objective-C(也就是ARC)的規(guī)則,哪些對象用 Core Foundation 的規(guī)則(也就是 MRC)的規(guī)則?;蛘哒f要確定對象類型轉(zhuǎn)換了之后,內(nèi)存管理的ownership 的改變。

If you cast between Objective-C and Core Foundation-style objects, you need to tell the compiler about the ownership semantics of the object using either a cast (defined in objc/runtime.h) or a Core Foundation-style macro (defined in NSObject.h)

于是蘋果在引入 ARC 之后對 Toll-Free Bridging 的操作也加入了對應(yīng)的方法與修飾符,用來指明用哪種規(guī)則管理內(nèi)存,或者說是內(nèi)存管理權(quán)的歸屬。

5.1 __bridge

只是聲明類型轉(zhuǎn)變,但是不做內(nèi)存管理規(guī)則的轉(zhuǎn)變。

示例:

CFStringRef s = (__bridge CFStringRef)[[NSString alloc] initWithFormat:@"Hi, %@!", name];

只是 NSString 到 CFStringRef 的類型轉(zhuǎn)化,但管理規(guī)則未變,依然要用 Objective-C 類型的 ARC 來管理 s,不能用CFRelease() 去釋放 s。

5.2 __bridge_retained、CFBridgingRetain()

將指針類型轉(zhuǎn)變的同時,將內(nèi)存管理的責任由原來的 Objective-C 交給 Core Foundation 來處理,也就是,將 ARC 轉(zhuǎn)變?yōu)?MRC。

示例:

NSString * s1 = [[NSString alloc] initWithFormat:@"Hi, %@!", name];
CFStringRef s2 = (__bridge_retained CFStringRef)s1;
...
CFRelease(s2);  // 注意在使用結(jié)束后釋放

在第二行做了轉(zhuǎn)化,這時內(nèi)存管理規(guī)則由 ARC 變成了 MRC,需要手動的來管理 s2 的內(nèi)存,而對于 s1,即使將其置為 nil,也不能釋放內(nèi)存。

也可以寫成:

NSString * s1 = [[NSString alloc] initWithFormat:@"Hi, %@!", name];
CFStringRef s2 = (CFStringRef)CFBridgingRetain(s1);
...
CFRelease(s2);  // 注意在使用結(jié)束后釋放

5.3 __bridge_transfer、CFBridgingRelease()

功能與 __bridge_retained 相反,表示將管理的責任由 Core Foundation 轉(zhuǎn)交給 Objective-C,即將管理方式由MRC 轉(zhuǎn)變?yōu)?ARC。

比如:

CFStringRef result = CFURLCreateStringByAddingPercentEscapes(. . .);
NSString * s = (__bridge_transfer NSString *)result; 
// 或 NSString * s = (NSString *)CFBridgingRelease(result);
return s;

這里將 result 的管理責任交給了 ARC 來處理,就不需要再顯式地調(diào)用 CFRelease() 了。

這里和 ARC 中 4 個主要的修飾符 __strong、__weak、__autoreleasing... 不同,這里修飾符的位置是放在類型前面的,雖然官方文檔中沒有說明,但最好與官方的相同。

arc

六、ARC下獲取引用計數(shù)

6.1 使用 KVC

[obj valueForKey:@"retainCount"];

6.2 使用私有 API

OBJC_EXTERN int _objc_rootRetainCount(id);

_objc_rootRetainCount(obj);

這個不一定完全可信。Xcode 10.1 用的示例一直返回 1。

6.3 使用 CFGetRetainCount

CFGetRetainCount((__bridge CFTypeRef)(obj))

使用 Toll-Free-Bridging 將 OC 對象的內(nèi)容管理轉(zhuǎn)為 Core Foundation 對象。

七、學習文章

iOS 開發(fā)ARC內(nèi)存管理技術(shù)要點
談 Objective-C block的實現(xiàn)
block 的實現(xiàn)
正確使用 Block 避免Cycle Retain和Crash
ARC 的實現(xiàn)原理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容