《Objective-C高級編程》自動引用計數 閱讀筆記 item2(ARC規則及其實現)

《Objective-C高級編程》自動引用計數 閱讀筆記系列

《Objective-C高級編程》自動引用計數 閱讀筆記 item1(內存管理/引用計數)
《Objective-C高級編程》自動引用計數 閱讀筆記 item2(ARC規則及其實現)

1.3 ARC規則

1.3.1 設置ARC有效的編譯方法如下:

  • 使用clang(LLVM編譯器)3.0或以上版本
  • 指定編譯器屬性為"-fobjc-arc"

1.3.2 引用計數式內存管理的思考方式就是在ARC有效時同樣適用

1.3.3 所有權修飾符

概念

對象類型:*** 指向NSObject這樣的Objective-C類的指針,如“NSObject * ”。 **
id類型:*** 用于隱藏對象類型的類名部分,相當于C語言中的“void *”。 **

ARC有效時,對象類型和id類型必須附加所有權修飾符,如下:

  • __strong修飾符
  • __weak修飾符
  • __unsafe_unretained修飾符
  • __autoreleasing修飾符

*** __strong修飾符 ***

__strong修飾符是id類型和對象類型默認的所有權修飾符。
源碼如下:

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

附有__strong修飾符的變量obj在超出其變量作用域時,即在該變量被廢棄時,會釋放其被賦予對象。

關鍵知識點:

  • __strong修飾符表示對對象的”強引用“。持有強引用的變量在超出其作用域時被廢棄,隨著強引用的失效,引用的對象會隨之釋放。
  • 附有__strong修飾符的變量之間可以相互賦值。通過相互賦值,可以使得變量對對象的強引用失效,從而釋放原先持有的對象,轉而持有由另外一個變量賦值的新的對象的強引用。
  • 即時是Objective-C類成員變量,也可以在方法參數上,使用附有__strong修飾符的變量
  • __strong修飾符可以確保將附有__strong修飾符的自動變量(局部變量)初始化為nil(該規則適用于__weak修飾符和__autoreleasing修飾符)

*** 通過__strong修飾符使ARC有效遵循了Objective-C內存管理的思考方式 ***

  1. “自己生成的對象,自己持有”和“非自己生成的對象,自己也能持有”只需通過對帶__strong修飾符的變量賦值便可達成
  2. 通過廢棄帶__strong修飾符的變量(變量作用域結束或是成員變量所屬對象廢棄)或者對變量賦值,都可以做到”不再需要自己持有的對象時釋放“
  3. 由于不必再次鍵入release,所以”非自己持有的對象無法釋放“原本就不會執行

__strong修飾符的不足

__strong修飾符無法解決引用計數式內存管理中存在的問題——循環引用。循環引用容易發生內存泄露(內存泄露:應當廢棄的對象在超出其生存周期后繼續存在)。

  • 類成員變量的循環引用:
Snip20160126_12.png
  • 自引用:
Snip20160126_13.png

*** __weak修飾符 ***

__weak修飾符提供弱引用。弱引用不能持有對象。

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

實際編譯以上代碼,編譯器會報錯,如下:

Snip20160126_14.png

解釋:將自己生成并持有的對象賦值給帶__weak修飾符的變量obj,變量obj會立即釋放生成的對象。
解決方式:將對象賦值給附有__strong修飾符的變量之后再賦值給附有__weak修飾符的變量,如下:

{
    id __strong obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
}

由于帶__weak修飾符的變量(弱引用)不持有對象,所以超出其變量作用域時,對象即被釋放。

__weak修飾符的優點

  • 可以避免發生循環引用


    Snip20160126_15.png
  • 在持有“對象”的弱引用時,若該對象被廢棄,則此弱引用將自動失效且處于nil被賦值的狀態(空弱引用),如以下代碼:
id __weak obj1 = nil;

{
    id __strong obj0 = [[NSObject alloc] init];
    
    obj1 = obj0;  // obj1變量持有NSObject對象的弱引用
    
    NSLog(@“A : %@”, obj1);  
    // output A : <NSObject: 0x731e180>
}

NSLog(@“B : %@”, obj1; // output B : (null)
  • 通過檢查附有__weak修飾符的變量是否為nil,可以判斷被賦值的對象是否已廢棄
  • __weak修飾符只能用于iOS5以上及OS X Lion以上版本的應用程序

*** __unsafe_unretained修飾符 ***

源代碼分析:

id __unsafe_unretained obj1 = nil;
{
    id __strong obj0 = [[NSObject alloc] init];
    
    obj1 = obj0; 
    // obj1變量即不持有對象的強引用也不持有弱引用
    
    NSLog(@"A: %@", obj1);
    // 輸出obj1變量表示的對象
    
}
/*
 * 因為obj0變量超出其作用域,強引用失效,
 * 所以自動釋放自己持有的對象
 * 因為對象無持有者,所以廢棄該對象
 */

 NSLog(@"B: %@", obj1);
 /*
  * 輸出obj1變量表示的對象
  * 
  * obj1表示的對象已經被廢棄(懸垂指針)!
  * 錯誤訪問!
  */
  • 附有 __unsafe_unretained修飾符的變量不屬于編譯器的內存管理對象。
  • 附有__unsafe_unretained修飾符的變量同附有__weak修飾符的變量一樣,因為自己生成并持有的對象不能繼續為自己所有,所以生成的對象會立即被釋放。
  • 訪問了已經被廢棄的對象,只有在個別運行情況下應用程序才會崩潰。
  • 在使用__unsafe_unretained修飾符時,賦值給附有__strong修飾符的變量時有必要確保被賦值的對象確實存在
  • 賦值給附有__unsafe_unretained修飾符變量的對象在通過該變量使用時,如果沒有確保其確實存在,那么應用程序就會崩潰

*** __autoreleasing修飾符 ***

  • 在ARC有效時,用@autoreleasepool塊替代NSAutoreleasePool類,用附有_autoreleasing修飾符的變量替代autorelease方法,如下:


    Snip20160128_17.png
  • 不需要顯式地附加__autoreleasing修飾符,因為編譯器會檢查方法名是否已alloc/new/copy/mutableCopy方法開始,如果不是則自動將返回值的對象注冊到autoreleasePool。
@autoreleasePool{
    
    id __strong obj = [NSMutableArray array];
    
    /*
     * obj變量持有對象的強引用
     *
     * 并且該對象由編譯器判斷其方法名后,
     * 自動注冊到autoreleasePool
     */
    
}/*
  * obj變量超出其作用域,強引用失效
  * 所以自動釋放自己持有的對象
  * 
  * 同時,隨著@autoreleasePool塊的結束,
  * 注冊到autoreleasePool中的所有對象被自動釋放
  * 
  * 因為對象的所有者不存在,所以廢棄該對象
  */

  • 訪問附有__weak修飾符的變量時,實際上必定要訪問注冊到autoreleasePool的對象。
  • id的指針或對象的指針在沒有顯式指定時會被附加上_autoreleasing修飾符,如下:
指針 帶__autoreleasing修飾符
id的指針id *obj id __autoreleasing *obj
對象的指針NSObject **obj NSObject *__autoreleasing *obj
  • 為了得到詳細的錯誤信息,經常在方法的參數中傳遞NSError對象的指針,而不是函數返回值。
// 方法聲明
- (Bool)performOperationWithError:(NSError **)error;
// 等同于
- (Bool)performOperationWithError:(NSError *__autoreleasing *)error;

// 應用
NSError *error = nil; 
Bool result = [obj performOperationWithError:&error];

// 上述源代碼經過編譯器的轉化:
NSError __strong *error = nil;
NSError __autoreleasing *tmp = error;
Bool result = [obj performOperationWithError:&tmp];
error = tmp;

/*
 編譯器正是通過這種添加源代碼的方式使得原先的源代碼即不會編譯出錯,也能在使用  參數取得對象時,貫徹內存管理的思考方式。
 */
  • 在顯式地指定__autoreleasing修飾符時,必須注意對象變量要為自動變量(包括局部變量、函數以及方法參數)

1.3.4 ARC的規則

  • 不能使用retain/release/retainCount/autorelease
  • 不能使用NSAllocateObject/NSDeallocateObject
  • 須遵守內存管理的方法命名規則
  • 不要顯式調用dealloc
  • 使用@autoreleasePool替代NSAutoreleasePool
  • 不能使用區域(NSZone)
  • 對象型變量不能作為C語言結構體(struct/union)的成員
  • 顯式轉換“id”和“void”

*** 須遵守內存管理的方法命名規則 ***

在ARC有效時,用于對象生成/持有的方法除了alloc/new/copy/mutableCopy還要追加一條命名規則init。
該方法是實例方法,并且必須要返回對象。返回的對象應為id類型或該方法聲明類的對象類型,或是該類的超類型或子類型。該返回對象并不注冊到autoreleasePool上。基本上只是對alloc方法返回值的對象進行初始化處理并返回該對象。

*** 不要顯式調用dealloc ***

  • 對象被廢棄時,不管ARC是否有效,都會調用對象的dealloc方法。在dealloc方法里記述廢棄對象時所需做的處理。
  • dealloc方法需要通過free來釋放留出的內存。
  • dealloc方法在大多數情況下適用于刪除已注冊的代理或觀察者對象。

*** 對象型變量不能作為C語言結構體(struct/union)的成員 ***

要把對象型變量加入到結構體成員中時,可強制轉換為void * 或是附加__unsafe_unretained修飾符

*** 顯式轉換“id”和“void” ***

  • id類型或對象類型變量賦值給void *或者逆向賦值時都需要進行特定的轉換。如果只想單純地賦值,則可以使用"__bridge轉換"。如void *p = (__bridge void *)obj;
  • __bridge_retained轉換可使要轉換賦值的變量也持有所賦值的對象。與retain類似。
void *p = 0;
{
    id obj = [[NSObject alloc] init]; // 對象的retaidedCount : 1
    
    p = (__bridge_retained void *)obj; // 對象的retainedCount : 2
    
}
// 作用域結束時,obj變量持有對象的強引用失效,所以釋放持有對象,但是由于__bridge_retained轉換使變量p看上去處于持有該對象的狀態,因此該對象不會被廢棄
NSLog(@"class = %@", [(__bridge id)p class]);
  • __bridge_transfer,被轉換的變量所持有的對象在該變量被賦值給轉換目標變量后隨之釋放。與release類似。
  • 這些轉換多數使用在Objective-C對象和Core Foundation對象之間的相互變換中。

1.3.5 屬性

當ARC有效時,屬性聲明的屬性與所有權修飾符的對應關系,如下表:

屬性聲明的屬性 所有權修飾符
assign __unsafe_unretained修飾符
copy __strong修飾符(但是賦值的是被復制的對象)
retain __strong修飾符
strong __strong修飾符
unsafe_unretianed __unsafe_unretained修飾符
weak __weak修飾符

1.3.6 數組

  • __unsafe_unretained修飾符以外的__strong/__weak/__autoreleasing修飾符保證其指定的變量初始化為nil。同樣地,附有__strong/__weak/__autoreleasing修飾符變量的數組也保證其初始化為nil。如下,將附有__strong修飾符的變量作為靜態數組使用:id objs[10];

  • 將附有__strong修飾符的變量作為動態數組使用時,根據不同的目的選擇使用NSMutableArray、NSMutableDictionary、NSMutableSet等Foundation框架的容器。這些容器會恰當地持有追加的對象并為我們管理這些對象。

  • 在C語言的動態數組使用附有__strong修飾符的變量,必須遵守一些事項:

    1. 聲明動態數組用指針,如id __strong *array = nil;由于不能保證附有__strong修飾符的id指針類型變量被初始化為nil,需要自行賦值nil。
    2. 使用類名時如下,NSObject *__strong *array = nil;
  • 使用calloc函數確保想分配的附有__strong修飾符變量的容量占有的內存塊,如array = (id __strong *)calloc(entries, sizeof(id));

  • 在動態數組中操作附有__strong修飾符的變量與靜態數組有很大差異,需要自己釋放所有的元素。在用free函數廢棄數組用內存塊前,需要先將nil賦值給所有元素,使得元素所賦值對象的強引用失效,從而釋放那些對象,再用free函數廢棄內存塊。

  • 使用附有__weak修飾符變量的動態數組與使用__strong修飾符相像。

  • 在__autoreleasing修飾符的情況下,最好不要使用動態數組。

  • 由于__unsafe_unretained修飾符在編譯器的內存管理對象之外,所以它與void *類型一樣,只能作為C語言的指針類型來使用.

1.4 ARC的實現

ARC由以下工具、庫來實現:

  • clang(LLVM編譯器)3.0以上
  • objc4 Objective-C運行時庫493.9以上

1.4.1 __strong修飾符

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

以上源代碼可轉換為調用以下的函數,

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

總結

如上所示,*** 在ARC有效時不能使用release方法的緣故是編譯器會自動插入release。***

NSMutableArray類的array類方法的調用如下:

{
    id __strong obj = [NSMutableArray array];
}

轉換后的源代碼是

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

其中,NSMutableArray類的array類方法:

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

轉換后的源代碼是

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

總結

如上所示,*** objc_retainAutoreleasedReturnValue函數和objc_autoreleaseReturnValue函數的協作,可以不將對象注冊到autoreleasePool中而直接傳遞,這一過程達到了最優化。***
如圖所示:


Snip20160128_19.png

關鍵知識點:

  • objc_autoreleaseReturnValue函數用于alloc/new/copy/mutableCopy方法以外的NSMutableArray類的array類方法等返回對象的實現上。

  • objc_autoreleaseReturnValue函數與objc_autorelease函數不同,一般不僅限于注冊對象到autoreleasePool中。

  • objc_retainAutoreleasedReturnValue函數主要用于最優化程序運行。即,它是用于自己持有(retain)對象的函數,但它持有的對象應為返回注冊在autoreleasePool中對象的方法或是函數的返回值。

  • objc_retainAutoreleasedReturnValue函數與objc_retain函數不同,它即時不注冊到autoreleasePool而返回對象,也能夠正確地獲取對象。

  • objc_autoreleaseReturnValue函數會檢查使用該函數的方法或函數調用方的執行命令列表,如果方法或函數的調用方在調用了方法或函數后緊跟著調用objc_retainAutoreleasedReturnValue函數,那么就不將返回的對象注冊到autoreleasePool中,而是直接傳遞到方法或函數的調用方。

1.4.2 __weak修飾符

  • 若附有__weak修飾符的變量所引用的對象被廢棄,則將nil賦值給該變量。
  • 使用附有__weak修飾符的變量,即是使用注冊到autoreleasePool中的對象。

假設obj附有__strong修飾符且對象被賦值

{   
    id __weak obj1 = obj;
}

轉換后的源代碼是

/* 編譯器的模擬代碼 */
id obj1;
objc_initWeak(&obj1, obj);
objc_destroyWeak(&obj1);

其中,objc_initWeak函數將附有__weak修飾符的變量初始化為0后,會將賦值的對象作為參數調用objc_storeWeak函數:

obj1 = 0;
objc_storeWeak(&obj1, obj);

objc_destroyWeak函數將0作為參數調用objc_storeWeak函數:

objc_storeWeak(%obj1, 0);

即,轉換后的源代碼也可以是

/* 編譯器的模擬代碼 */
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
objc_storeWeak(%obj1, 0);

總結

  • 通過objc_initWeak函數初始化附有__weak修飾符的變量,在變量作用域結束時通過objc_destroyWeak函數釋放該變量。
  • objc_destroyWeak函數把第二參數的賦值對象的地址作為鍵值,將第一參數的附有__weak修飾符的變量的地址注冊到weak表中。如果第二參數為0,則把變量的地址從weak表中刪除。

釋放對象,廢棄誰都不持有的對象時,程序的動作:

  1. objc_release
  2. 因為引用計數為0所以執行dealloc
  3. _objc_rootDealloc
  4. object_dispose
  5. objc_destructInstance
  6. objc_clear_deallocating

其中,對象被廢棄時最后調用的objc_clear_deallocating函數的動作如下:

  1. 從weak表中獲取廢棄對象的地址為鍵值的記錄
  2. 將包含在記錄中的所有附有__weak修飾符變量的地址,賦值為nil(釋放變量)
  3. 從weak表中刪除該記錄
  4. 從引用計數表中刪除廢棄對象的地址為鍵值的記錄(廢棄對象)

注意

  • 附有__weak修飾符的變量所引用的對象被廢棄時,則將nil賦值給該變量。如果大量使用附有__weak修飾符的變量,則會消耗相應的CPU資源。最好是只在需要避免循環引用時使用__weak修飾符。
  • 將自己生成并持有的對象直接賦值給附有__weak修飾符的變量,編譯器會報錯,如下:
{
    id __weak obj = [[NSObject alloc] init];
}

obj變量不能持有該對象,這時會被釋放并被廢棄,引起編譯器的警告。

Snip20160129_1.png

通過轉換后的源代碼解釋,

/* 編譯器的模擬代碼  */
id obj;
id tmp = objc_msgSend(NSObject, @selctor(alloc));
objc_msgSend(tmp, @selctor(init));
objc_initWeak(&obj, tmp);
objc_release(tmp); // 釋放該對象
objc_destroyWeak(&object); // 廢棄該對象

總結

  • 雖然自己生成并持有的對象通過objc_initWeak函數被賦值給附有__weak修飾符的變量中,但編譯器判斷其沒有持有者,故該對象立即通過objc_release函數被釋放和廢棄。
  • nil會被賦值給引用廢棄對象的附有__weak修飾符的變量中。

假設obj附有__strong修飾符且對象被賦值

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

轉換后的源代碼是

/* 編譯器的模擬代碼 */
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);

其中,與前面的被賦值時相比,在使用附有__weak修飾符變量的情形下,增加了對objc_loadWeakRetained函數和objc_autorelease函數的調用。這兩個函數的動作如下:

  1. objc_loadWeakRetained函數取出附有__weak修飾符所引用的對象并retain。
  2. objc_autorelease函數將對象注冊到autoreleasepool中。

注意

  • 因為附有__weak修飾符變量所引起的對象像這樣被注冊到autoreleasepool中,所以在@autoreleasepool塊結束之前都可以放心使用。
  • 如果大量地使用附有__weak修飾符的變量,注冊到autoreleasepool的對象也會大量地增加,因此在使用附有__weak修飾符的變量時,最好先暫時賦值給附有__strong修飾符的變量后再使用。如
{
   id __weak o = obj;
   id tmp = o;
   NSLog(@"1 %@", tmp);
   NSLog(@"2 %@", tmp);
   NSLog(@"3 %@", tmp);
}

1.4.3 __autoreleasing修飾符

將對象賦值給附有__autoreleasing修飾符的變量等同于ARC無效時調用對象的autorelease方法。

alloc/new/copy/mutableCopy方法群:

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

轉換后的源代碼是

/* 編譯器模擬代碼  */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selctor(alloc));
objc_msgSend(obj, @selctor(init));
objc_autorelease(obj); // 將對象添加到autoreleasepool中
objc_autoreleasePoolPop(pool);

NSMutableArray類的array類方法:

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

轉換后的源代碼是

/* 編譯器的模擬代碼  */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray, @selctor(array));
objc_retainAutoreleasedReturnValue(obj);//用于自己持有(retain)對象
objc_autorelease(obj); // 將對象添加到autoreleasepool中
objc_autoreleasePoolPop(pool);

1.4.4 引用計數

  • 由于弱引用并不持有對象,所以賦值給附有__weak修飾符的變量中也必定不會改變引用計數數值。
  • 通過__autoreleasing修飾符向autoreleasepool注冊時,引用計數數值會加1,但是@autoreleasepool塊結束時會釋放已注冊的對象,引用計數數值會減1。
  • 不使用__autoreleasing修飾符,僅使用附有__weak修飾符聲明的變量也能將引用對象注冊到autoreleasepool中。
  • 不能完全信任_objc_rootRetainCount函數取得的數值。

后記

由于該書講解知識點并沒有完整的實例,所以還需要再找另外的書籍或者博客進行練習。個人更偏向于書籍,精通Objective-C第4、5、6章有相關內存管理和ARC方面的講解和練習。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容