Objective-C 記憶體管理之ARC規則

小白HAKU發表於2017-12-07

基本概念

ARC為自動引用計數,引用計數式記憶體管理的本質並沒有改變,ARC只是自動處理“引用計數”的相關部分。

在編譯上,可以設定ARC有效或無效。預設工程中ARC有效,若設定無效,則指定編譯器屬性為-fno-objc-arc


記憶體管理思考方式

同引用計數相同

  • 自己生成的物件,自己持有
  • 非自己生成的物件,自己也能持有
  • 自己持有的物件不需要時釋放
  • 非自己持有的物件無法釋放

所有權修飾符

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing

__stong修飾符

__strong修飾符是id型別和物件型別預設的所有權修飾符。 也就是說,以下程式碼中的id變數實際上都被家了所有權修飾符

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

在ARC無效時,寫法雖然同ARC有效時一致,但

{
    id __strong obj = [[NSObject alloc] init];
}
複製程式碼

以上程式碼明確指定了變數的作用域。

ARC無效時,該原始碼可記述如下:

/** ARC無效 */
{
    id obj =[[NSObject alloc] init];
    [obj release];
}
複製程式碼

為了釋放生成並持有的物件,增加呼叫了release方法的程式碼。該原始碼進行的動作跟ARC有效時的動作完全一樣。

如上所示,附有__strong修飾符的變數obj在超出其變數作用域是,即在該變數被廢棄是,會釋放其被賦予的物件。

__strong修飾符表示對物件的“強引用”。持有強你用的變數在超出其作用域時被廢棄,雖則會強引用的失效,引用的物件會隨之釋放。

/**自己生成並持有物件*/
{
    id __strong obj = [[NSObject alloc] init];
    /**因為變數obj為強引用,所以自己持有物件*/
}
/*
 *因為變數obj超出其作用域,強引用失效,所以自動釋放自己持有的物件。
 *物件的所有者不存在,因為廢棄該物件。
 */
複製程式碼

取得非自己生成並持有的物件時,程式碼如下:

/**取得非自己生成並持有的物件*/
{
    id __strong obj = [NSMutableArray array];
    /**因為變數obj為強引用,所以自己持有物件*/
}
/**因為變數obj超出其作用範圍,強引用失效,所以自動地釋放自己持有的物件*/
複製程式碼

變數的賦值如下:

id __strong obj0 = [[NSObject alloc] init]; /**物件A*/
/**obj0持有物件A的強引用*/

id __strong obj1 = [[NSObject alloc] init]; /**物件B*/
/**obj1持有物件B的強引用*/

id __strong obj2 = nil;
/**obj2不持有任何物件*/

obj0 = obj1;
/*
 *obj0持有由obj1賦值的物件B的強引用
 *因為obj0被賦值,所以原先持有物件A的強引用失效。
 *物件A的所有者不存在,因為廢棄物件A.
 *此時,持有物件B的強引用的變數為obj0和obj1
 */
 
obj2 = obj0;
/*
 *obj2持有由obj0賦值的物件B的強引用
 *此時,持有物件B的強引用的變數為obj0,obj1和obj2;
 */
 
obj1 = nil;
/*
 *因為nil被賦予了obj1,所以對物件B的強引用失效。
 *此時,持有物件B的強引用的變數為obj0和obj2;
 */
 
obj0 = nil;
/*
 *因為nil被賦予了obj1,所以對物件B的強引用失效。
 *此時,持有物件B的強引用的變數為obj0和obj2;
 */
 
obj2 = nil;
/*
 *因為nil被賦予了obj2,所以對物件B的強引用失效。
 *因為物件B的所有者不存在,所以廢棄物件B.
 */

複製程式碼

__strong修飾符的變數,不僅只在變數作用域中,在賦值上也能夠正確得管理其物件的所有者。同樣,在方法的引數上,也可以使用附有__strong修飾符的變數。

@interface Test : NSObjecgt
{
    id __strong obj_;
}
- (void) setObject:(id __strong)obj;
@end

@implementation Test
- (id) init
{
    self = [super init];
    return self;
}

- (void) setObject:(id __strong)obj
{
    obj_ = obj;
}
@end
複製程式碼

使用該類如下:

{
    id __strong test = [[Test alloc] init];
    /*
     *test 持有Test物件的強引用
     */
    [test setObject:[[NSObject alloc] init];
    /*
     *Test物件的obj_成員,
     *持有NSObject物件的引用;
     */
}
/*
 *因為test變數超出其作用域,強引用失效,所以自動釋放Test物件。
 *Test物件的所有者不存在,因此廢棄該物件。
 *廢棄Test物件的同時,Test的成員變數obj_也被廢棄,
 *NSObject物件的強引用失效,自動釋放NSObject物件。
 *NSObject物件的所有者不存在,因此廢棄該物件。
 */

    
複製程式碼

另外,__strong修飾符同__weak __autoreleasing,可以保證將附有這些修飾符的自動變數初始化為nil。

通過__strong修飾符,不必再次retain或者release,就可以滿足引用計數記憶體管理的思考方式。

  • 自己生成的物件自己持有
  • 非自己生成的物件,自己也可以持有
  • 不再需要自己持有的物件時釋放
  • 非自己持有的物件無法釋放

__weak修飾符

僅僅通過__strong修飾符不能夠解決引用計數記憶體管理中的“迴圈引用”問題。

例如:

@interface Test : NSObjecgt
{
    id __strong obj_;
}
- (void) setObject:(id __strong)obj;
@end

@implementation Test
- (id) init
{
    self = [super init];
    return self;
}

- (void) setObject:(id __strong)obj
{
    obj_ = obj;
}
@end
複製程式碼

以下為迴圈引用:

{
    id test0 = [[Test alloc] init];/* 物件A */
    /*
     * test0 持有Test物件A的強引用
     */
     
    id test1 = [[Test alloc] init];/* 物件B */
    /*
     * test1 持有Test物件B的強引用
     */
     
    [test0 setObject:test1];
    /*
     * Test物件A的obj_成員變數持有Test物件B的強引用
     * 此時,持有Test物件B的強引用的變數為Test物件A的obj_和test1
     */
     
    [test1 setObject:test0];
    /*
     * Test物件B的obj_成員變數持有Test物件A的強引用
     * 此時,持有Test物件A的強引用的變數為Test物件B的obj_和test0
     */
     
}
/*
 * 因為test0變數超出其作用域,強引用失效,所以自動釋放Test物件A
 * 因為test1變數超出其作用域,強引用失效,所以自動釋放Test物件B
 * 此時持有Test物件A的強引用的變數為Test物件B的obj_
 * 此時持有Test物件B的強引用的變數為Test物件A的obj_
 * 發生記憶體洩漏
 */

複製程式碼

迴圈引用容易發生記憶體洩漏。記憶體洩漏就是應當廢棄的物件在超出其生存週期後繼續存在。

以下程式碼也會引起記憶體洩漏(對自身的強引用)

id test = [[Test alloc] init];
[test setObject:test];

複製程式碼

使用__weak修飾符可以避免迴圈引用。

__weak修飾符與__strong修飾符相反,提供弱引用。弱引用不能持有物件例項。

id __weak obj = [[NSObject alloc] init];
複製程式碼

如果執行以上程式碼,編譯器會發出警告。

Assigning retained object to weak variable; object will be released after assignment
複製程式碼

以上程式碼將自己生成並持有的物件賦值給附有__weak修飾符的變數obj。即變數obj持有對持有物件的弱引用。因此,為了不以自己持有的狀態來儲存自己生成並持有的物件,生成的物件會被立即釋放。如果將物件賦值給附有__strong修飾符的變數之後再賦值給附有__weak修飾符的變數,就不會發生警告了。

{
    /**自己生成並持有物件*/
    id __strong obj0 = [[NSObject alloc] init];
    /** 因為obj0變數為強引用,所以自己持有物件 */
    
    id __weak obj1 = obj0;
    /** obj1變數持有生成物件的弱引用 */
}
/*
 *因為obj0變數超出其作用域,強引用失效,所以自動釋放自己持有的物件。
 *因為物件的所有者不存在,所以廢棄該物件。
 */
複製程式碼

因為帶__weak修飾符的變數(弱引用)不持有物件,所以在超出其變數作用域時,物件即被釋放。如下程式碼即可避免迴圈引用。

@interface Test : NSObject
{
    id __weak obj_;
}
- (void) setObject:(id __strong) obj;
@end
複製程式碼

__weak修飾符還有另一優點:在持有某物件的弱引用時,若該物件被廢棄,則此弱引用將自動失效且處於nil被賦值的狀態(空弱引用)。如下所示:

id __weak obj1 = nil;
{
    /**自己生成並持有物件*/
    
    id __strong obj0 = [[NSObject alloc] init];
    
    /**因為obj0變數為強引用,所以自己持有物件*/
    
    obj1 = obj0;
    
    /**obj1持有物件的弱引用*/
    
    NSLog(@"A: %@",obj1);
    /** 輸出obj1變數持有的弱引用的物件*/
}
/*
 *因為obj0變數超出其作用域,強引用失效,所以自動釋放自己持有的物件。
 *因為物件無持有者,所以廢棄該物件。
 *廢棄物件的同時,持有該物件弱引用的obj1變數的弱引用失效,nil賦值給obj1.
 */

NSLog(@"B: %@",obj1);
/** 輸出賦值給obj1變數中的nil */

複製程式碼

該程式碼的執行結果為:

2017-12-05 20:13:28.458858+0800 ImageOrientation[6316:1604800] A: <NSObject: 0x604000207710>
2017-12-05 20:13:30.112086+0800 ImageOrientation[6316:1604800] B: (null)
複製程式碼

以上,使用__weak修飾符可以避免迴圈引用。通過檢查__weak修飾符的變數是否為nil,可以判斷被賦值的物件是否以廢棄。

__unsafe_unretained 修飾符

__unsafe_unretained修飾符的變數部署與編譯器的記憶體管理物件。(ARC式的記憶體管理式編譯器的工作)

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

如果執行以上程式碼,編譯器會發出警告。雖然使用了unsafe的變數,但是編譯器並不會忽略。

Assigning retained object to unsafe_unretained variable; object will be released after assignment

附有__unsafe_unretained修飾符的變數同附有__weak修飾符的變數一樣,因為自己生成並持有的物件不能繼續為自己所有,所以生成的物件會立即釋放。

id __unsafe_unretained obj1 = nil;
{
    /**自己生成並持有物件*/
    
    id __strong obj0 = [[NSObject alloc] init];
    
    /**因為obj0變數為強引用,所以自己持有物件*/
    
    obj1 = obj0;
    
    /**雖然obj0變數賦值給obj1,但obj1變數既不持有隊形的強引用,也不持有物件的弱引用*/
    
    NSLog(@"A: %@",obj1);
    /** 輸出obj1變數表示的物件*/
}
/*
 *因為obj0變數超出其作用域,強引用失效,所以自動釋放自己持有的物件。
 *因為物件無持有者,所以廢棄該物件。
 */

NSLog(@"B: %@",obj1);
/*
 * 輸出obj1變數表示的物件
 *
 * obj1變數表示的物件已經被廢棄(懸垂指標)
 * 指向曾經存在的物件,但該物件已經不再存在了,此類指標稱為懸垂指標。結果未定義,往往導致程式錯誤,而且難以檢測。
 * 錯誤訪問
 */

複製程式碼

該程式碼的執行結果為:

2017-12-06 08:45:54.005966+0800 ImageOrientation[6859:1736666] A: <NSObject: 0x604000011f00>

執行到NSLog(@"B: %@",obj1)時crash:Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)

複製程式碼

__autoreleasing 修飾符

ARC有效時,不能使用autorelease方法,也不能使用NSAutoreleasePool類。雖然autorelease無法直接使用,但是autorelease功能是起作用的。

ARC無效時程式碼如下:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autoreleae];
[pool drain];
複製程式碼

ARC有效時,程式碼可以寫成下面這樣:

@autoreleaepool{
    id __autoreleasing obj = [[NSObject alloc] init];
}
複製程式碼

指定“@autoreleasepool塊”來代替“NSAutoreleasePool類物件生成,持有以及廢棄”這一範圍。

另外,ARC有效時,要通過將物件賦值給附加了 __autoreleaing修飾符的變數來代替呼叫autorelease方法。物件賦值給附有__autoreleasing修飾符的變數等價於在ARC無效時呼叫物件的autorelease方法,即物件被註冊到autoreleasepool。

也就是說可以理解為,在ARC有效時,用@autoreleasepool塊替代NSAutoreleasePool類,用附有__autoreleasing修飾符的變數替代autorelease方法。

一般來說,不會顯示地附加__autoreleasing修飾符。在取得非自己生成並持有的物件時,索然可以使用alloc/new/copy/mutableCopy以外的方法來取得物件,但該物件已經被註冊到了autoreleasepool。這是由於編譯器會檢查方法名是否以alloc/new/copy/mutableCopy開始,如果不是則自動將返回值的物件註冊到autoreleasepool。(init方法返回值的物件不註冊到autoreleasepool)

@autoreleasepool{
    /** 取得非自己生成並持有的物件*/
    id __strong obj = [MutableArray array];
    /*
     * 因為變數obj為強引用,所以自己持有物件。
     * 並且該物件由編譯器判斷其方法名後自動註冊到autoreleasepool
     */
}
/*
 * 因為變數obj超出其作用域,強引用失效,所以自動釋放自己持有的物件。
 * 同時,隨著@autoreleasepool塊的結束,註冊到autoreleasepool中的所有物件被自動釋放。
 * 因為物件的所有者不存在,所以廢棄物件。
 */
複製程式碼

訪問附有__weak修飾符的變數時,必須訪問註冊到autoreleasepool的物件。

id __weak obj1 = obj0;
NSLog(@"class = %@",[obj1 class]);
複製程式碼

等同於

id __weak obj1 = obj0;
id __autoreleasing temp = obj1;
NSLog(@"class = %@",[temp class]);
複製程式碼

因為__weak修飾符只持有物件的弱引用,而在訪問引用物件的過程中,該物件有可能被廢棄。如果要把訪問的物件註冊到autoreleasepool中,那麼在@autoreleasepool塊結束之前都能確保該物件存愛。因此,在使用附有__weak修飾符的變數時就必定要shying註冊到autoreleasepool中的物件。

同樣地,id的指標或者物件的指標在沒有顯示指定時會被附加上__autoreleasing修飾符。


ARC 規則

在ARC有效的情況下編譯原始碼,必須遵守以下規則:

  • 不能使用retain/release/retainCount/autorelease
  • 不能使用NSAllocateObject/NSDeallocateObject
  • 必須遵守記憶體管理的方法命名規則
  • 不要顯示呼叫dealloc
  • 使用@autoreleasepool塊代替NSAutoreleasePool
  • 不能使用區域(NSZone)
  • 物件型變數不能作為C語言結構體(struct/union)的成員
  • 顯示轉換"id"和"void"

不能使用retain/release/retainCount/autorelease

記憶體管理是編譯器的工作,因為沒有必要使用記憶體管理的方法(retain/release/retainCount/autorelease)。只能在ARC無效且手動進行記憶體管理時才能使用。

不能使用NSAllocateObject/NSDeallocateObject

一般通過呼叫NSObject類的alloc類方法來生成並持有OC物件。alloc類方法實際上是通過直接呼叫NSAllocateObject函式來生成並持有物件的。

須遵守記憶體管理的方法命名規則

在ARC無效時,用於物件生成/持有的方法必須遵循以下命名規則:以alloc/new/copy/mutablCopy開始的方法在返回物件時,必須返回給呼叫方所應當持有的物件。

在ARC有效時,除了上述規則外,增加init規則。

以init開始的方法的規則要比alloc/new/copy/mutableCopy更嚴格。該方法必須是例項方法,並且必須要返回物件。返回的物件應該為id型別或者該方法宣告類的物件型別。

id obj = [[NSObject alloc] init];

如上所示,init方法會初始化alloc方法返回的物件,然後原封不動地返還給呼叫方。

注:initialize不包含在上述命名規則裡。

不要顯式呼叫dealloc

無論ARC是否有效,只要物件的所有者都不持有該物件,該物件就被廢棄。物件被廢棄時,無論ARC是否有效,都會呼叫物件的dealloc方法。在ARC無效時,必須呼叫[super dealloc]。ARC有效時會遵循無法顯式呼叫dealloc這一規則,ARC對此會自動進行處理,dealloc中只需技術廢棄物件時所必須的處理,比如刪除已註冊的代理或者觀察者物件。

使用@autoreleasepool塊代替NSAutoreleasePoll

不能使用區域(NSZone)

物件型變數不能作為C語言結構體的成員

struct Data{
    NSMutableArray * array;
}
複製程式碼

編譯後

error:ARC forbids Objective-C objects in struct

因為C語言的規約上沒有方法來管理結構體成員的生存週期。

要把物件型變數假如到結構體成員中,可強制轉換為void*或者是附加__unsafe_unretained修飾符。(附有__unsafe_unretained修飾符的變數不屬於編譯器的記憶體管理物件)

顯式轉換id和void*

ARC無效時,id變數和void*變數相互賦值沒有問題,但是ARC有效時則會引起編譯錯誤。

在ARC有效時,id型或物件型變數賦值給void*或者逆向賦值時都需要進行特定的轉換。如果想單純地賦值,可以使用__bridige轉換。

id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
id o = (__bridge id)p;
複製程式碼

但是轉換為void*的__bridge轉換,其安全性與賦值給__unsafe_unretained修飾符相近,甚至會更低。如果管理時不注意賦值物件的所有者,就會因懸垂指標而導致程式崩潰。

__bridge轉換還有其他兩種:__bridge_retained轉換和__bridge_transfer轉換

__bridge_retained轉換可使要轉換賦值的變數也持有所賦值的物件。__bridge_retained轉換與retain相類似。

void *p = 0;
{
    id obj = [[NSObject alloc] init];
    p = (__bridge_retained void *)obj;
}
NSLog(@"class = %@",[(__bridge id)p class]);
複製程式碼

執行結果為: 2017-12-06 16:39:06.148058+0800 ImageOrientation[7685:2312365] class = NSObject

變數作用域結束時,雖然隨著持有強引用的變數obj失效,物件隨之釋放,但由於__bridge_retained轉換使變數p看上去持有該物件的狀態,因此該物件不會被廢棄。

ARC無效時實現以上邏輯的程式碼為:

void *p = 0;
{
    id obj = [[NSObject alloc] init];
    /** [obj retainCount] ->1 */
    p = [obj retain];
    /** [obj retainCount] ->2 */
    [obj release];
    /** [obj retainCount] ->1 */
}
/*
 * [(id)p retainCount] ->1
 * 即[obj retainCount] ->1
 * 物件仍存在
 */
複製程式碼

__bridge_transfer轉換,被轉換的變數所持有的物件在該變數被賦值給轉換目標變數後隨之釋放。__bridge_transfer轉換與release相類似。

id obj = (__bridge_transfer id)p;相當於ARC無效時程式碼如下:

/**ARC無效*/
id obj = (id)p;
[obj retain];
[p release];
複製程式碼

__bridge轉換,__bridge_retained轉換,__bridge_transfer轉換常用於OC物件和CoreFoundation物件之間的相互變換中。

  1. __bridge:CF和OC物件轉化時只涉及物件型別不涉及物件所有權的轉化;
  2. __bridge_transfer:常用在講CF物件轉換成OC物件時,將CF物件的所有權交給OC物件,此時ARC就能自動管理該記憶體;(作用同CFBridgingRelease())
  3. __bridge_retained:(與__bridge_transfer相反)常用在將OC物件轉換成CF物件時,將OC物件的所有權交給CF物件來管理;(作用同CFBridgingRetain())

以下函式可用於OC物件和CoreFoundation物件之間的相互變換,成為Toll-Free Bridge "免費橋"轉換。

CFTypeRef CFBridgingRetain(id X){
    return (__bridge_retained CFTypeRef)X;
}

id CFBridgingRelease(CFTypeRef X){
    return (__bridge_transfer id)X;
}
複製程式碼

下例為將生成並持有的NSMutableArray物件作為CoreFoundation物件來處理。

CFMutableArrayRef cfObject = NULL;
{
    id obj = [[NSMutableArray alloc] init
    /**變數obj持有對生成並持有物件的強引用 */
    
    cfObject = CFBridgingRetain(obj);
    /** 通過CFBridgingRetain將物件CFRetain賦值給變數cfObject */
    
    CFShow(cfObject);
    printf("retain count =%d\n",CFGetRetainCount(cfObject));
    /** 通過變數obj的強引用和通過CFBridgingRetain,引用計數為2
}
/** 因為變數obj超出作用範圍,所以其強引用失效,引用計數為1 */
printf("retain count after the scope =%d\n",CFGetRetainCount(cfObject));

CFRelease(cfObject);
/** 因為將物件CFRelease,所以其引用計數為0,古該物件被廢棄。*/


複製程式碼

執行結果為:

(
)
retain count =2
retain count after the scope =1
複製程式碼

由此可知,Foundation框架的API生成並持有的OC物件能夠作為CF物件來使用,也可以通過CFRelease來釋放。以上程式碼也可以用__bridge_retained轉換來代替。

CFMutableArrayRef cfObject = (__bridge_retained CFMutableArrayRef) obj;

如果使用__bridge轉換,第一句列印的結果是1,因為__bridge轉換不改變物件的持有狀況,obj持有NSMutableArray物件的強引用,所以為1.後一句列印crash,因為obj超出其作用域,所以強引用失效,物件釋放,無持有者的物件被廢棄,所以出現懸垂指標,導致崩潰。

那麼,將CoreFoundation的API生成並持有物件,將該物件作為NSMutableArray物件來處理,程式碼如下:

{
    CFMutableArrayRef cfObject = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
    printf("retain count =%d\n",CFGetRetainCount(cfObject));
    /*
     * CoreFoundation框架的API生成並持有物件
     * 之後的物件引用計數為1
     */
    id obj = CFBridgingRelease(cfObject);
    /*
     * 通過CFBridgingRelease賦值,變數obj持有物件強引用的同時,物件通過CFRelease釋放
     */
    
    printf("retain count after the cast =%d\n",CFGetRetainCount(cfObject));
    /*
     * 因為只有變數obj持有對生成並持有物件的強引用,所以引用計數為1
     * 另外,經由CFBridgingRelease轉換後,賦值給變數cfObject中的指標也指向仍在存在的物件,所以可以正常使用。
     */
    NSLog(@"class =%@",obj);
    
}
/*
 * 因為變數obj超出作用域,所以其強引用失效,物件得到釋放,無所有者的物件隨之被廢棄。
 */
複製程式碼

執行結果如下:

retain count =1
retain count after the cast =1
2017-12-06 21:30:02.473804+0800 ImageOrientation[8249:2573155] class =(
)
複製程式碼

也可以使用__bridge_transfer轉換代替CFBridgingRelease。

id obj = (__bridge_transfer id)cfObject;
複製程式碼

如果使用__bridge來替代__bridge_transfer或CFBridgingRelease轉換,則第一句列印為1,中間列印為2,因為obj和cfObject同時持有物件的強引用,所以為2。超出範圍後obj強引用失效,物件的引用計數為1。


ARC屬性

ARC有效時,Objective-C類的屬性也會發生變化。具體如下所以:

屬性宣告的屬性 所有權修飾符
assign __unsafe_unretained修飾符
copy __strong修飾符(但是賦值的是被複制的物件)
ratai __strong修飾符
unsafe_unretained __unsafe_unretained修飾符
weak __weak修飾符

以上各屬性複製給指定的屬性中就想彈鼓賦值給附加各屬性對應的所有權修飾符的變數中。只有copy屬性不是簡單的賦值,他賦值的是通過NSCopying介面的copyWithZone:方法複製複製源所生成的物件。

相關文章