__weak 修飾符

就像前面咱們看到的同樣,__weak 修飾符提供的功能如同魔法通常。shell


1,若附有__weak 修飾符的變量所引用的對象被廢棄,則將nil 賦值給該變量。express

2,使用附有__weak 修飾符的變量,便是使用註冊到autoreleasepool 中的對象。編程


1,若附有__weak 修飾符的變量所引用的對象被廢棄,則將nil 賦值給該變量。

這些功能像魔法同樣,到底發生了什麼,咱們一無所知。因此下面咱們來看看它們的實現。
框架

{   
     id __weak obj1 = obj;
}

假設變量obj 附加__strong 修飾符且對象被賦值。
函數

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

經過objc_initWeak 函數初始化附有_ _ weak 修飾符的變量,在變量做用域結束時經過objc_destroyWeak 函數釋放該變量。
spa

如如下源代碼所示,objc_initWeak 函數將附有_ _ weak 修飾符的變量初始化爲0 後,會將賦值的對象做爲參數調用objc_storeWeak 函數。
指針

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

//objc_destroyWeak 函數將0 做爲參數調用objc_storeWeak 函數。
objc_storeWeak(&obj1, 0);

即前面的源代碼與下列源代碼相同。
code

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

objc_storeWeak 函數把第二參數的賦值對象的地址做爲鍵值,將第一參數的附有_ _ weak 修飾符的變量的地址註冊到weak 表中。orm

若是第二參數爲0,則把變量的地址從weak 表中刪除。對象

weak 表與引用計數表(參考1.2.4 節)相同,做爲散列表被實現。若是使用weak 表,將廢棄對象的地址做爲鍵值進行檢索,就能高速地獲取對應的附有_ _ weak 修飾符的變量的地址。

另外,因爲一個對象可同時賦值給多個附有_ _ weak 修飾符的變量中,因此對於一個鍵值,可註冊多個(weak)變量的地址。

釋放對象時,廢棄誰都不持有的對象的同時,程序的動做是怎樣的呢?下面咱們來跟蹤觀察。對象將經過objc_release 函數釋放。

(1)objc_release

(2)由於引用計數爲0 因此執行dealloc

(3)_objc_rootDealloc

(4)object_dispose

(5)objc_destructInstance

(6)objc_clear_deallocating

對象被廢棄時最後調用的objc_clear_deallocating 函數的動做以下:

  1. (1)從weak 表中獲取廢棄對象的地址爲鍵值的記錄。

  2. (2)將包含在記錄中的全部附有_ _ weak 修飾符變量的地址,賦值爲nil。

  3. (3)從weak 表中刪除該記錄。

  4. (4)從引用計數表中刪除廢棄對象的地址爲鍵值的記錄。


根據以上步驟,前面說的若是附有_ _ weak 修飾符的變量所引用的對象被廢棄,則將nil 賦值給該變量這一功能即被實現。由此可知,若是大量使用附有_ _ weak 修飾符的變量,則會消耗相應的CPU 資源。良策是隻在須要避免循環引用時使用_ _ weak 修飾符。

使用_ _ weak 修飾符時,如下源代碼會引發編譯器警告。

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


由於該源代碼將本身生成並持有的對象賦值給附有_ _ weak 修飾符的變量中,因此本身不能持有該對象,這時會被釋放並被廢棄,所以會引發編譯器警告。

warning: assigning retained obj to weak variable; obj will be
      released after assignment [-Warc-unsafe-retained-assign]
        id __weak obj = [[NSObject alloc] init];
                  ^     ~~~~~~~~~~~~~~


編譯器如何處理該源代碼呢?

/* 編譯器的模擬代碼 */
id obj;
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_initWeak(&obj, tmp);
objc_release(tmp); // 由於此時只有一個強引用,因此對象被釋放了
objc_destroyWeak(&object);

雖然本身生成並持有的對象經過objc_initWeak 函數被賦值給附有_ _ weak 修飾符的變量中,但編譯器判斷其沒有持有者(即 強引用),故該對象當即經過objc_release 函數被釋放和廢棄。

這樣一來,nil 就會被賦值給引用廢棄對象的附有_ _ weak 修飾符的變量中。

下面咱們經過NSLog 函數來驗證一下。

{
    id __weak obj = [[NSObject alloc] init];
    NSLog(@"obj=%@", obj);
}


如下爲該源代碼的輸出結果,其中用%@ 輸出nil。

obj=(null)


關於「當即釋放對象」

如前所述,如下源代碼會引發編譯器警告。

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


這是因爲編譯器判斷生成並持有的對象不能繼續持有。附有__unsafe_unretained修飾符的變量又如何呢?

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


與__weak修飾符徹底相同,編譯器判斷生成並持有的對象不能繼續持有,從而發出警告。
warning: assigning retained object to unsafe_unretained variable;
      obj will be released after assignment [-Warc-unsafe-retained-assign]
    id __unsafe_unretained obj = [[NSObject alloc] init];
                           ^     ~~~~~~~~~~~~~~~~~~~~~~~

該源代碼經過編譯器轉換爲如下形式。

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


objc_release函數當即釋放了生成並持有的對象,這樣該對象的懸垂指針被賦值給變量obj中。

那麼若是最初不賦值變量又會如何呢?下面的源代碼在ARC無效時一定會發生內存泄漏。
[[NSObject alloc] init];

因爲源代碼不使用返回值的對象,因此編譯器發出警告。
warning: expression result unused [-Wunused-value]
    [[NSObject alloc] init];
    ^~~~~~~~~~~~~~~~~~~~~~~

可像下面這樣經過向void型轉換來避免發生警告。
(void)[[NSObject alloc] init];

不論是否轉換爲void,該源代碼都會轉換爲如下形式
/* 編譯器的模擬代碼 */
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_release(tmp);

雖然沒有指定賦值變量,但與賦值給附有__unsafe_unretained修飾符變量的源代碼徹底相同。因爲不能繼續持有生成並持有的對象,因此編譯器生成了當即調用objc_release函數的源代碼。而因爲ARC的處理,這樣的源代碼也不會形成內存泄漏。

另外,能調用被當即釋放的對象的實例方法嗎?
(void)[[[NSObject alloc] init] hash];

該源代碼可變爲以下形式:
/* 編譯器的模擬代碼 */
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_msgSend(tmp, @selector(hash));
objc_release(tmp);

在調用了生成並持有對象的實例方法後,該對象被釋放。看來「由編譯器進行內存管理」這句話應該是正確的。

2,此次咱們再用附有_ _ weak 修飾符的變量來確認另外一功能:使用附有_ _ weak 修飾符的變量,便是使用註冊到autoreleasepool 中的對象。

{
    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 修飾符的變量後再使用。

好比,如下源代碼使用了5 次附有_ _ weak 修飾符的變量o。

{
    id __weak o = obj;
    NSLog(@"1 %@", o);
    NSLog(@"2 %@", o);
    NSLog(@"3 %@", o);
    NSLog(@"4 %@", o);
    NSLog(@"5 %@", o);
}


相應地,變量o 所賦值的對象也就註冊到autoreleasepool 中5 次。

objc[14481]: ##############
objc[14481]: AUTORELEASE POOLS for thread 0xad0892c0
objc[14481]: 6 releases pending.
objc[14481]: [0x6a85000]  ................  PAGE  (hot) (cold)
objc[14481]: [0x6a85028]  ################  POOL 0x6a85028
objc[14481]: [0x6a8502c]         0x6719e40  NSObject
objc[14481]: [0x6a85030]         0x6719e40  NSObject
objc[14481]: [0x6a85034]         0x6719e40  NSObject
objc[14481]:  [0x6a85038]         0x6719e40  NSObject
objc[14481]: [0x6a8503c]         0x6719e40  NSObject
objc[14481]: ##############


將附有_ _ w e a k 修飾符的變量o 賦值給附有_ _ s t r o n g 修飾符的變量後再使用能夠避免此類問題。

{
    id __weak o = obj;
    id tmp = o;
    NSLog(@"1 %@", tmp);
    NSLog(@"2 %@", tmp);
    NSLog(@"3 %@", tmp);
    NSLog(@"4 %@", tmp);
    NSLog(@"5 %@", tmp);
}

「tmp = o;」時對象僅登陸到autoreleasepool 中1 次。

objc[14481]: ##############
objc[14481]: AUTORELEASE POOLS for thread 0xad0892c0
objc[14481]: 2 releases pending.
objc[14481]: [0x6a85000]  ................  PAGE  (hot) (cold)
objc[14481]: [0x6a85028]  ################  POOL 0x6a85028
objc[14481]: [0x6a8502c]         0x6719e40  NSObject
objc[14481]: ##############


在iOS4 和OS  X  Snow  Leopard 中是不能使用_ _ weak 修飾符的,而有時在其餘環境下也不能使用。實際上存在着不支持_ _ weak 修飾符的類。

例如NSMachPort 類就是不支持_ _ weak 修飾符的類。這些類重寫了retain/release 並實現該類獨自的引用計數機制。可是賦值以及使用附有_ _ weak 修飾符的變量都必須恰當地使用objc4運行時庫中的函數,所以獨自實現引用計數機制的類大多不支持_ _ weak 修飾符。

不支持_ _ weak 修飾符的類,其類聲明中附加了「_ _ attribute_ _ ((objc_arc_weak_reference_unavailable))」這一屬性,同時定義了NS_AUTOMATED_REFCOUNT_WEAK_UNAVAILABLE。

若是將不支持_ _ weak 聲明類的對象賦值給附有_ _ weak 修飾符的變量,那麼一旦編譯器檢驗出來就會報告編譯錯誤。並且在Cocoa 框架類中,不支持_ _ weak 修飾符的類極爲罕見,所以沒有必要太過擔憂。

專欄allowsWeakReference/retainWeakReference 方法實際上還有一種狀況也不能使用__weak修飾符。

就是當allowsWeakReference/retainWeakReference實例方法(沒有寫入NSObject接口說明文檔中)返回NO的狀況。這些方法的聲明以下:
- (BOOL)allowsWeakReference;
- (BOOL)retainWeakReference;

在賦值給__weak修飾符的變量時,若是賦值對象的allowsWeakReference方法返回NO,程序將異常終止。

cannot form weak reference to instance (0x753e180) of class MyObject即對於全部allowsWeakReference方法返回NO的類都絕對不能使用__weak修飾符。這樣的類一定在其參考說明中有所記述。

另外,在使用__weak修飾符的變量時,當被賦值對象的retainWeakReference方法返回NO的狀況下,該變量將使用「nil」。如如下的源代碼:

{
    id __strong obj = [[NSObjectalloc] init];
    id __weak o = obj;
    NSLog(@"1 %@", o);
    NSLog(@"2 %@", o);
    NSLog(@"3 %@", o);
    NSLog(@"4 %@", o);
    NSLog(@"5 %@", o);
}


因爲最開始生成並持有的對象爲附有__strong修飾符變量obj所持有的強引用,因此在該變量做用域結束以前都始終存在。所以以下所示,在變量做用域結束以前,能夠持續使用附有__weak修飾符的變量o所引用的對象。
1 <NSObject: 0x753e180>
2 <NSObject: 0x753e180>
3 <NSObject: 0x753e180>
4 <NSObject: 0x753e180>
5 <NSObject: 0x753e180>

下面對retainWeakReference方法進行試驗。咱們作一個MyObject類,讓其繼承NSObject類並實現retainWeakReference方法。

@interfaceMyObject : NSObject
{
    NSUInteger count;
}
@end

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

- (BOOL)retainWeakReference
{
    if (++count > 3)
        return NO;
    return [super retainWeakReference];
}

@end


該例中,當retainWeakReference方法被調用4次或4次以上時返回NO。在以前的源代碼中,將從NSObject類生成並持有對象的部分更改成MyObject類。
{
    id __strong obj = [[MyObject alloc] init];
    id __weak o = obj;
    NSLog(@"1 %@", o);
    NSLog(@"2 %@", o);
    NSLog(@"3 %@", o);
    NSLog(@"4 %@", o);
    NSLog(@"5 %@", o);
}
如下爲執行結果。
1 <MyObject: 0x753e180>
2 <MyObject: 0x753e180>
3 <MyObject: 0x753e180>
4 (null)
5 (null)
從第4次起,使用附有__weak修飾符的變量o時,因爲所引用對象的retainWeakRef-erence方法返回NO,因此沒法獲取對象。像這樣的類也一定在其參考說明中有所記述。

另外,運行時庫爲了操做__weak修飾符在執行過程當中調用allowsWeakReference/retainWeakReference方法,所以從該方法中再次操做運行時庫時,其操做內容會永久等待。本來這些方法並無記入文檔,所以應用程序編程人員不可能實現該方法羣,但若是因某些緣由而不得不實現,那麼仍是在所有理解的基礎上實現比較好。

相關文章
相關標籤/搜索