iOSARC記憶體管理要點

秋伏發表於2016-01-20

前言

在討論 ARC 之前,我們需要知道 Objective-C 採用的是引用計數式的記憶體管理方式,這一方式的特點是:

  • 自己生成的物件自己持有。比如:NSObject * __strong object = [NSObject alloc] init];
  • 非自己生成的物件自己也能持有。比如:NSMutableArray * __strong array = [NSMutableArray array];
  • 自己持有的物件不再需要時釋放。
  • 非自己持有的物件自己無法釋放。

而 ARC 則是幫助我們做物件記憶體管理的一套機制,使得我們以前在 MRC 模式下管理記憶體工作量能在 ARC 模式下得到緩解。正如蘋果官方文件上所描述的:

Automatic Reference Counting (ARC) is a compiler feature that provides automatic memory management of Objective-C objects.

可見 ARC 是編譯時特性,它沒有改變 Objective-C 引用計數式記憶體管理的本質,更不是 GC(垃圾回收)。

在 ARC 特性下有 4 種與記憶體管理息息相關的變數所有權修飾符值得我們關注:

  • __strong
  • __weak
  • __autoreleasing
  • __unsafe_unretaied

說到變數所有權修飾符,有人可能會跟屬性修飾符搞混,這裡做一個對照關係小結:

  • assign 對應的所有權型別是 __unsafe_unretained
  • copy 對應的所有權型別是 __strong
  • retain 對應的所有權型別是 __strong
  • strong 對應的所有權型別是 __strong
  • unsafe_unretained 對應的所有權型別是 __unsafe_unretained
  • weak 對應的所有權型別是 __weak

以上除了 weak 外,其他的屬性修飾符在 MRC 模式下也是有效的。

另外,__strong__weak__autoreleasing 修飾的自動變數會自動初始化為 nil。

接下來就一一介紹 4 種變數所有權修飾符。

關於 __strong

__strong 表示強引用,對應定義 property 時用到的 strong。當物件沒有任何一個強引用指向它時,它才會被釋放。如果在宣告引用時不加修飾符,那麼引用將預設是強引用。當需要釋放強引用指向的物件時,需要保證所有指向物件強引用置為 nil。__strong 修飾符是 id 型別和物件型別預設的所有權修飾符。

關於 __weak

__weak 表示弱引用,對應定義 property 時用到的 weak。弱引用不會影響物件的釋放,而當物件被釋放時,所有指向它的弱引用都會自定被置為 nil,這樣可以防止野指標。__weak 最常見的一個作用就是用來避免強引用迴圈。但是需要注意的是,__weak 修飾符只能用於 iOS5 以上的版本,在 iOS4 及更低的版本中使用 __unsafe_unretained 修飾符來代替。

__weak 的幾個使用場景:

  • 在 Delegate 關係中防止強引用迴圈。在 ARC 特性下,通常我們應該設定 Delegate 屬性為 weak 的。但是這裡有一個疑問,我們常用到的 UITableView 的 delegate 屬性是這樣定義的:@property (nonatomic, assign) id<UITableViewDelegate> delegate;,為什麼用的修飾符是 assign 而不是weak?其實這個 assign 在 ARC 中意義等同於 __unsafe_unretaied(後面會講到),它是為了在 ARC 特性下相容 iOS4 及更低版本來實現弱引用機制。一般情況下,你應該儘量使用 weak
  • 在 Block 中防止強引用迴圈。
  • 用來修飾指向由 Interface Builder 建立的控制元件。比如:@property (weak, nonatomic) IBOutlet UIButton *testButton;

關於 __autoreleasing

在 ARC 模式下,我們不能顯示的使用 autorelease 方法了,但是 autorelease 的機制還是有效的,通過將物件賦給__autoreleasing 修飾的變數就能達到在 MRC 模式下呼叫物件的 autorelease 方法同樣的效果。

__autoreleasing 修飾的物件會被註冊到 Autorelease Pool 中,並在 Autorelease Pool 銷燬時被釋放,和 MRC 特性下的 autorelease 的意義相同。定義 property 時不能使用這個修飾符,因為任何一個物件的 property 都不應該是 autorelease 型別的。

在 ARC 模式下,顯式的使用 __autoreleasing 的場景很少見,但是 autorelease 的機制卻依然在很多地方默默起著作用。我們來看看這些場景:

  • 方法返回值。
  • 訪問 __weak 修飾的變數。
  • id 的指標或物件的指標(id *)。

方法返回值

示例

首先,我們看這個方法:

- (NSObject *)object {
    NSObject *o = [[NSObject alloc] init];

    return o;
}

這裡 o 的所有權修飾符是預設的 __strong。由於 return 使得 o 超出其作用域,它強引用持有的物件本該被釋放,但是由於該物件作為函式返回值,所以**一般情況下**編譯器會自動將其註冊到 Autorelease Pool 中(注意這裡是一般情況下,在一些特定情況下,ARC 機制提出了巧妙的執行時優化方案來跳過 autorelease 機制,見後面章節)。這是 autorelease 機制默默起作用的一個例子。

方法返回值時的 autorelease 機制

那麼這裡有一個問題:為什麼方法返回值的時候需要用到 autorelease 機制呢?

這涉及到兩個角色的問題。一個角色是呼叫方法接收返回值的接收方。當引數被作為返回值 return 之後,接收方如果要接著使用它就需要強引用它,使它 retainCount +1,用完後再清理,使它 retainCount -1。有持有就有清理,這是接收方的責任。另一個角色就是返回物件的方法,即提供方。在方法中建立了物件並作為返回值時,一方面你建立了這個物件你就得負責釋放它,有建立就有釋放,這是建立者的責任。另一方面你得保證返回時物件沒被釋放以便方法外的接收方能拿到有效的物件,否則你返回的是 nil,有何意義呢。所以就需要找一個合理的機制既能延長這個物件的生命週期,又能保證對其釋放。這個機制就是 autorelease 機制

當物件作為引數從方法返回時,會被放到正在使用的 Autorelease Pool 中,由這個 Autorelease Pool 強引用這個物件而不是立即釋放,從而延長了物件的生命週期,Autorelease Pool 自己銷燬的時候會把它裡面的物件都順手清理掉,從而保證了物件會被釋放。但是這裡也引出另一個問題:既然會延長物件的生命週期到 Autorelease Pool 被銷燬的時候,那麼 Autorelease Pool 的生命週期是多久呢?會不會在 Autorelease Pool 都銷燬了,接收方還沒接收到物件呢?

Autorelease Pool 是與執行緒一一對映的,這就是說一個 autoreleased 的物件的延遲釋放是發生在它所在的 Autorelease Pool 對應的執行緒上的。因此,在方法返回值的這個場景中,如果 Autorelease Pool 的 drain 方法沒有在接收方和提供方交接的過程中觸發,那麼 autoreleased 物件是不會被釋放的(除非嚴重錯亂的使用執行緒)。

通常,Autorelease Pool 的銷燬會被安排在很好的時間點上:

  • Run Loop 會在每次 loop 到尾部時銷燬 Autorelease Pool。
  • GCD 的 dispatched blocks 會在一個 Autorelease Pool 的上下文中執行,這個 Autorelease Pool 不時的就被銷燬了(依賴於實現細節)。NSOperationQueue 也是類似。
  • 其他執行緒則會各自對他們對應的 Autorelease Pool 的生命週期負責。

至此,我們知道了為何方法返回值需要 autorelease 機制,以及這一機制是如何保障接收方能從提供方那裡獲得依然鮮活的物件。

ARC 模式下方法返回值跳過 autorelease 機制的優化方案

在 MRC 時代,當我們自己建立了物件並把它作為方法的返回值返回出去時,需要手動呼叫物件的 autorelease 方法,如上節所講的利用 autorelease 機制正確返回物件。到了 ARC 時代,ARC 需要保持對 MRC 程式碼的相容,這就意味著 MRC 的實現和 ARC 的實現可以相互替換,而物件接收方和物件提供方無需知道對方是 MRC 實現還是 ARC 實現也能正確工作。比如,當基於 MRC 實現的程式碼呼叫你的一個 ARC 實現的方法來獲取一個物件,那麼你的方法必須同樣採用上文所講的 autorelease 機制來返回物件以確保物件接收方能正確獲得物件。所以,即使在 ARC 模式下物件的 autorelease 方法不再能被顯示呼叫,但是 autorelease 的機制仍然是在默默的工作著,只是編譯器在幫你實踐這一機制。

但是,ARC 還提出了巧妙的執行時優化方案來跳過 autorelease 機制。這個過程是這樣的:當方法的呼叫方和實現方的程式碼都是基於 ARC 實現的時候,在方法 return 的時候,ARC 會呼叫 objc_autoreleaseReturnValue() 替代前面說的autorelease。在呼叫方持有方法返回物件的時候(也就是做 retain 的時候),ARC 會呼叫objc_retainAutoreleasedReturnValue()。在呼叫 objc_autoreleaseReturnValue() 時,它會在棧上查詢 return address 來確定 return value 是否會被傳給 objc_retainAutoreleasedReturnValue()。如果沒傳,那麼它就會走前文所講的 autorelease 的過程。如果傳了(這表明返回值能順利從提供方交接給接收方),那麼它就跳過 autorelease並同時修改 return address 來跳過 objc_retainAutoreleasedReturnValue(),從而一舉消除了 autorelease 和retain 的過程。這個方案可以在 MRC-to-ARC 呼叫、ARC-to-ARC 呼叫以及 ARC-to-MRC 呼叫中正確工作,並在符合條件的一些 ARC-to-ARC 呼叫中消除 autorelease 機制。

訪問 __weak 修飾的變數

在訪問 __weak 修飾的變數時,實際上必定會訪問註冊到 Autorelease Pool 的物件。如下來年兩段程式碼是相同的效果:

id __weak obj1 = obj0;
NSLog(@"class=%@", [obj1 class]);
// 等同於:
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);

為什麼會這樣呢?因為 __weak 修飾符只持有物件的弱引用,而在訪問物件的過程中,該物件有可能被廢棄,如果把被訪問的物件註冊到 Autorelease Pool 中,就能保證 Autorelease Pool 被銷燬前物件是存在的。

id 的指標或物件的指標(id *)

另一個隱式地使用 __autoreleasing 的例子就是使用 id 的指標或物件的指標(id *) 的時候。

看一個最常見的例子:

NSError *__autoreleasing error; 
if (![data writeToFile:filename options:NSDataWritingAtomic error:&error]) { 
  NSLog(@"Error: %@", error); 
}

// 即使上面你沒有寫 __autoreleasing 來修飾 error,編譯器也會幫你做下面的事情:
NSError *error; 
NSError *__autoreleasing tempError = error; // 編譯器新增 
if (![data writeToFile:filename options:NSDataWritingAtomic error:&tempError]) 
{ 
  error = tempError; // 編譯器新增 
  NSLog(@"Error: %@", error); 
}

error 物件在你呼叫的方法中被建立,然後被放到 Autorelease Pool 中,等到使用結束後隨著 Autorelease Pool 的銷燬而釋放,所以函式外 error 物件的使用者不需要關心它的釋放。

在 ARC 中,所有這種指標的指標型別(id *)的函式引數如果不加修飾符,編譯器會預設將他們認定為__autoreleasing 型別。

有一點特別需要注意的是,某些類的方法會隱式地使用自己的 Autorelease Pool,在這種時候使用 __autoreleasing型別要特別小心。比如 NSDictionary 的 enumerateKeysAndObjectsUsingBlock 方法:

- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
    [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {

          // do stuff  
          if (there is some error && error != nil) {
                *error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
          }

    }];
}

上面的程式碼中其實會隱式地建立一個 Autorelease Pool,類似於:

- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
    [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {

          @autoreleasepool {  // 被隱式建立。
              if (there is some error && error != nil) {
                    *error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
              }
          }
    }];

    // *error 在這裡已經被dict的做列舉遍歷時建立的 Autorelease Pool釋放掉了。
} 

為了能夠正常的使用 *error,我們需要一個 strong 型別的臨時引用,在 dict 的列舉 Block 中是用這個臨時引用,保證引用指向的物件不會在出了 dict 的列舉 Block 後被釋放,正確的方式如下:

- (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error {
  NSError * __block tempError; // 加 __block 保證可以在Block內被修改。
  [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { 
    if (there is some error) { 
      *tempError = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil]; 
    }  

  }] 

  if (error != nil) { 
    *error = tempError; 
  } 
}

關於 __unsafe_unretained

ARC 是在 iOS5 引入的,而 __unsafe_unretained 這個修飾符主要是為了在 ARC 剛釋出時相容 iOS4 以及版本更低的系統,因為這些版本沒有弱引用機制。這個修飾符在定義 property 時對應的是unsafe_unretained__unsafe_unretained 修飾的指標純粹只是指向物件,沒有任何額外的操作,不會去持有物件使得物件的 retainCount +1。而在指向的物件被釋放時依然原原本本地指向原來的物件地址,不會被自動置為 nil,所以成為了野指標,非常不安全。

__unsafe_unretained 的應用場景:

  • 在 ARC 環境下但是要相容 iOS4.x 的版本,用 __unsafe_unretained 替代 __weak 解決強引用迴圈的問題。

ARC 模式規則

ARC 模式下,還有一些需要注意的規則:

  • 不能顯式使用 retain/release/retainCount/autorelease。
  • 不能使用 NSAllocateObject/NSDeallocateObject。
  • 需要遵守記憶體管理的方法命名規則。在 ARC 模式和 MRC 模式下,以 alloc/new/copy/mutableCopy 開頭的方法在返回物件時都必須返回給呼叫方所應當持有的物件。在 ARC 模式下,追加一條:以 init 開頭的方法必須是例項方法並且必須要返回物件。返回的物件應為 id 型別或宣告該方法的類的物件型別,或是該類的超型別或子型別。該返回的物件並不註冊到 Autorelease Pool 中,基本上只是對 alloc 方法返回值的物件進行初始化處理並返回該物件。需要注意的是:- (void)initialize; 方法雖然是以 init 開頭但是並不包含在上述規則中。
  • 不要顯式呼叫 dealloc。
  • 使用 @autoreleasepool 塊替代 NSAutoreleasePool。
  • 不能使用區域(NSZone)。
  • 物件型變數不能作為 C 語言結構體(struct/union)的成員。
  • 顯式轉換 id 和 void *

關於 Toll-Free Bridging

Toll-Free Briding 保證了在程式中,可以方便和諧的使用 Core Foundation 型別的物件和Objective-C 型別的物件。

在 MRC 時代,由於 Objective-C 型別的物件和 Core Foundation 型別的物件都是相同的 release 和 retain 操作規則,所以 Toll-Free Bridging 的使用比較簡單,但是自從切換到 ARC 後,Objective-C 型別的物件記憶體管理規則改變了,而 Core Foundation 依然是之前的機制,換句話說,Core Foundation 不支援 ARC。

這個時候就必須要要考慮一個問題了,在做 Core Foundation 與 Objective-C 型別轉換的時候,用哪一種規則來管理物件的記憶體。顯然,對於同一個物件,我們不能夠同時用兩種規則來管理,所以這裡就必須要確定一件事情:哪些物件用 Objective-C(也就是 ARC)的規則,哪些物件用 Core Foundation 的規則(也就是 MRC)的規則。或者說要確定物件型別轉換了之後,記憶體管理的 ownership 的改變。於是蘋果在引入 ARC 之後對 Toll-Free Bridging 的操作也加入了對應的方法與修飾符,用來指明用哪種規則管理記憶體,或者說是記憶體管理權的歸屬。這些方法和修飾符分別是:

  • __bridge(修飾符)
  • __bridge_retained(修飾符) or CFBridgingRetain(函式)
  • __bridge_transfer(修飾符) or CFBridgingRelease(函式)

__bridge

只是宣告型別轉變,但是不做記憶體管理規則的轉變。

比如:

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

只是做了 NSString 到 CFStringRef 的轉化,但管理規則未變,依然要用 Objective-C 型別的 ARC 來管理 s1,你不能用 CFRelease() 去釋放 s1。

__bridge_retained or CFBridgingRetain

表示將指標型別轉變的同時,將記憶體管理的責任由原來的 Objective-C 交給Core Foundation 來處理,也就是,將 ARC 轉變為 MRC。

比如,還是上面那個例子

NSString *s1 = [[NSString alloc] initWithFormat:@"Hello, %@!", name];
CFStringRef s2 = (__bridge_retained CFStringRef)s1;
// or CFStringRef s2 = (CFStringRef)CFBridgingRetain(s1);// do something with s2
//...CFRelease(s2); // 注意要在使用結束後加這個

我們在第二行做了轉化,這時記憶體管理規則由 ARC 變為了 MRC,我們需要手動的來管理 s2 的記憶體,而對於 s1,我們即使將其置為 nil,也不能釋放記憶體。

__bridge_transfer or CFBridgingRelease

這個修飾符和函式的功能和上面那個 __bridge_retained 相反,它表示將管理的責任由 Core Foundation 轉交給 Objective-C,即將管理方式由 MRC 轉變為 ARC。

比如:

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

這裡我們將 result 的管理責任交給了 ARC 來處理,我們就不需要再顯式地將 CFRelease() 了。

對了,這裡你可能會注意到一個細節,和 ARC 中那個 4 個主要的修飾符(__strong, __weak, …)不同,這裡修飾符的位置是放在型別前面的,雖然官方文件中沒有說明,但看官方的標頭檔案可以知道。記得別把位置寫錯。

toll_free_bridging

參考:


相關文章