Objective-C自動引用計數ARC

JiandanDream發表於2018-05-09

寫在前面

這篇文章是閱讀 Transitioning to ARC Release Notes 的筆記。

主要內容是關於 ARC 的規則。

簡介

Automatic Reference Counting(ARC) 作為一個編譯工具,自動管理 Objective-C 物件。

簡單地說,就是不再需要開發者使用 retain, release, autorelease 這些方法。

ARC 會在編譯時期,新增記憶體管理程式碼,確保物件儘可能地存活。

事實上,它使用的記憶體管理方式與 MRC 一致。

規則

  • 不能呼叫 dealloc,不能呼叫或者覆寫 retain, release, retainCount, autorelease 也不能使用 @selector(retain) 這樣的形式去呼叫。 可以覆寫 dealloc,去處理 ARC 未能進行管理的物件,但不需要呼叫 [super dealloc]。 Core Foundation 物件不受 ARC 管理,可繼續使用 CFRetain, CFRelease 進行管理。

  • 不能使用 NSAllocateObject 或 NSDeallocateObject 可以使用 alloc 建立物件,執行期系統管理需要銷燬的物件。

  • 不能在 C 結構體中,使用物件指標 建立 Objective-C 類去管理資料,而不是使用 struct。

  • 轉換 id 和 void * 需要特定規則

  • 不能使用 NSAutoreleasePool 物件 使用 @autoreleasepool{}

  • 不需要使用 NSZone

  • 存取方法名稱不能以 new 開頭

    // Won't work:
    @property NSString *newTitle;
    
    // Works:
    @property (getter=theNewTitle) NSString *newTitle;
    複製程式碼

生命週期修飾詞

變數修飾詞

  • __strong 預設值,被修飾物件會一直存活到:沒有強引用指向它
  • __weak 不會影響引用計數,當沒有指向的物件被銷燬時,指標會被設定成 nil
  • __unsafe_unretained 不保證修飾物件存活,當沒有強引用時,也不會設定為 nil,即使物件被銷燬,指標還是指向它
  • __autoreleasing 主要用來修飾傳遞引用的引數 常見的是傳遞 NSError 物件,返回後,NSError 物件會自動釋放。

正確使用形式

ClassName * qualifier variableName;
複製程式碼

其他形式在技術上來講是錯誤的,但編譯器“原諒”了它們。

當方法引數是個引用時,尤其需要注意,以下程式碼可以正常執行:

NSError *error;
BOOL OK = [myObject performOperationWithError:&error];
if (!OK) {
    // Report the error.
    // ...
}
複製程式碼

然而,實際 NSError 物件是這樣宣告的

NSError * __strong e;
複製程式碼

而其中的方法宣告是

-(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;
複製程式碼

所以,編譯器會重寫程式碼:

NSError * __strong error;
NSError * __autoreleasing tmp = error;
BOOL OK = [myObject performOperationWithError:&tmp];
error = tmp;
if (!OK) {
    // Report the error.
    // ...
}
複製程式碼

如果不希望編譯器這樣重寫程式碼的話,可以將 NSError 物件宣告成 __autoreleasing

使用修飾詞避免迴圈引用

使用 __weak

MyViewController *myController = [[MyViewController alloc] init];
// ...
MyViewController * __weak weakMyViewController = myController;
myController.completionHandler =  ^(NSInteger result) {
    // 新增 __strong,避免使用 weakMyViewController 時,它已經被釋放
    MyViewController * __strong strongMyViewController = weakMyViewController;
    [strongMyViewController dismissViewControllerAnimated:YES completion:nil];
};
複製程式碼

使用 __block,然後在 block 結束時,將引用的物件設為 nil。

MyViewController * __block myController = [[MyViewController alloc] init…];
// ...
myController.completionHandler =  ^(NSInteger result) {
    [myController dismissViewControllerAnimated:YES completion:nil];
    myController = nil;
};
複製程式碼

使用 @autoreleasepool{} 管理自動釋放池,比使用 NSAutoreleasePool 高效。

開發介面時,outlets 應該使用 weak 修飾。

棧上的變數,無論是 strong, weak 還是 autorelease,都預設初始化成 nil。

使用 -fobjc-arc 編譯器標誌來設定某個檔案,使用 ARC 環境。 使用 -fno-objc-arc 編譯器標誌來禁止某個檔案使用 ARC。

無縫橋接

  • __bridge 在 Objective-C 和 Core Foundation 物件間不轉換持有關係

  • __bridge_retained 或 CFBridgingRetain 可讓一個 Objective-C 指標轉換成 Core Foundation 指標,並持有它。 所以呼叫 CFRelease 或相關方法去釋放它

  • __bridge_transfer 或 CFBridgingRelease 將一個非 Objective-C 指標轉換成 Objective-C 指標,並持有它。 ARC 負責釋放它

以下程式碼,很好地展示了無縫橋接的使用:

- (void)drawRect:(CGRect)rect {
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
    CGFloat locations[2] = {0.0, 1.0};
    NSMutableArray *colors = [NSMutableArray arrayWithObject:(id)[[UIColor darkGrayColor] CGColor]];
    [colors addObject:(id)[[UIColor lightGrayColor] CGColor]];
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);
    CGColorSpaceRelease(colorSpace);  // Release owned Core Foundation object.
    CGPoint startPoint = CGPointMake(0.0, 0.0);
    CGPoint endPoint = CGPointMake(CGRectGetMaxX(self.bounds), CGRectGetMaxY(self.bounds));
    CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint,
                                kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
    CGGradientRelease(gradient);  // Release owned Core Foundation object.
}
複製程式碼

要點

  • 不能呼叫 retain, release, autorelease

  • 不能呼叫 dealloc

  • 不能使用 NSAutoreleasePool 物件 取而代之的是 @autoreleasepool{},它的執行效率是 NSAutoreleasePool 的6倍。

  • ARC 需要將 [super init] 的結果賦值給 self。 所以一般是這樣寫

    self = [super init];
    if (self) {
       ...
    }
    複製程式碼
  • 不需要實現 retain 或 release 方法 自定義 retain 或 release 會破壞弱指標。

目前 ARC 的效率已經足夠高了,若發現問題,可以提交 bug。

  • ARC 環境下,預設使用 strong 修飾符
  • 不能在 C 語言結構體裡使用 strong ids 替代方式:使用 Objective-C 物件替代。

如果不行,就將 Objective-C 物件轉換成 void* (使用 __unsafe_unretained 修飾)。

  • 不能直接轉換 id 和 void* (包括 Core Foundation),需要使用無縫橋接
struct x { NSString * __unsafe_unretained S; int X; }
複製程式碼

FAQ

blocks 在 ARC 下是如何工作的?

blocks 在它處於棧頂的時候工作,不需要呼叫 Block copy。

需要注意的是 NSString * __block myString 在 ARC 模式下,是會被持有的,而不是一個危險指標。

在 ARC 環境下,如何建立一個 C 語言陣列?

示例程式碼:

// Note calloc() to get zero-filled memory.
__strong SomeClass **dynamicArray = (__strong SomeClass **)calloc(entries, sizeof(SomeClass *));
for (int i = 0; i < entries; i++) {
     dynamicArray[i] = [[SomeClass alloc] init];
}
 
// When you're done, set each entry to nil to tell ARC to release the object.
for (int i = 0; i < entries; i++) {
     dynamicArray[i] = nil;
}
free(dynamicArray);
複製程式碼

需要注意的是:

  • 有時需要新增 __strong SomeClass **,因為預設是 __autoreleasing SomeClass **
  • 被分配的記憶體必須是 zero-filled
  • 在釋放陣列時,必須將每個物件設定成 nil(無法使用 memset 或 bzero)
  • 你應該要避免使用 memcpyrealloc
ARC 是否會比較慢

這跟如何使用有關係,但一般來說,不會慢。

編譯器高效地減少無關的 ratain 或 release 呼叫,而且做了很多加速 Objective-C 執行的工作。

特別地,在 ARC 下,呼叫 “return a retain/autoreleased objec” 的物件,也不會每次都被放到自動釋放池中。

可以在 debug 模式下,建立大量物件,讓 reatain 和 release 不斷被呼叫,可以觀察到,使用的時間接近0。

ARC 是否支援 ObjC++ 模式?

支援。 可以將 strong / weak 物件放到類或容器裡,ARC 編譯器會把 retain / release 邏輯複製到“copy constructors and destructors” 中。

哪些類不支援弱引用?

NSATSTypesetter, NSColorSpace, NSFont, NSMenuView, NSParagraphStyle, NSSimpleHorizontalTypesetter, and NSTextView.

在這種情況下,宣告屬性,需要使用 assign 代替 weak;宣告變數,需要使用 __unsafe_unretained。

當繼承 NSCell 或其他使用 NSCopyObject 的類,需要額外做些什麼?

不需要。

在 ARC 下,所有的複製方法僅僅複製例項變數。

相關文章