摘要 block中用到的外部變量最好使用 __weak 修飾,避免記憶體洩露;block容易引起引用循環的根本原因是: 1,對于(block内部用到的)外部變量,對其執行retain 的時機 與該block的執行時機是不同步的,在block聲明的時候就對外部變量進行了retain,而block何時執行甚至是否執行都是不可預測的;2,block 一般是匿名的,而且copy指派的,手動釋放block對象比較困難
目錄[-]
- 1,若附有__weak 修飾符的變量所引用的對象被廢棄,則将nil 指派給該變量。
- 2,這次我們再用附有_ _ weak 修飾符的變量來确認另一功能:使用附有_ _ weak 修飾符的變量,即是使用注冊到autoreleasepool 中的對象。
就像前面我們看到的一樣,__weak 修飾符提供的功能如同魔法一般。
1,若附有__weak 修飾符的變量所引用的對象被廢棄,則将nil 指派給該變量。
2,使用附有__weak 修飾符的變量,即是使用注冊到autoreleasepool 中的對象。
1,若附有__weak 修飾符的變量所引用的對象被廢棄,則将nil 指派給該變量。
這些功能像魔法一樣,到底發生了什麼,我們一無所知。是以下面我們來看看它們的實作。
?
1 2 3 | |
假設變量obj 附加__strong 修飾符且對象被指派。
?
1 2 3 4 | |
通過objc_initWeak 函數初始化附有_ _ weak 修飾符的變量,在變量作用域結束時通過objc_destroyWeak 函數釋放該變量。
如以下源代碼所示,objc_initWeak 函數将附有_ _ weak 修飾符的變量初始化為0 後,會将指派的對象作為參數調用objc_storeWeak 函數。
?
1 2 3 4 5 | |
即前面的源代碼與下列源代碼相同。
?
1 2 3 4 | |
objc_storeWeak 函數把第二參數的指派對象的位址作為鍵值,将第一參數的附有_ _ weak 修飾符的變量的位址注冊到weak 表中。
如果第二參數為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)從weak 表中擷取廢棄對象的位址為鍵值的記錄。
- (2)将包含在記錄中的所有附有_ _ weak 修飾符變量的位址,指派為nil。
- (3)從weak 表中删除該記錄。
- (4)從引用計數表中删除廢棄對象的位址為鍵值的記錄。
根據以上步驟,前面說的如果附有_ _ weak 修飾符的變量所引用的對象被廢棄,則将nil 指派給該變量這一功能即被實作。由此可知,如果大量使用附有_ _ weak 修飾符的變量,則會消耗相應的CPU 資源。良策是隻在需要避免循環引用時使用_ _ weak 修飾符。
使用_ _ weak 修飾符時,以下源代碼會引起編譯器警告。
?
1 2 3 | |
因為該源代碼将自己生成并持有的對象指派給附有_ _ weak 修飾符的變量中,是以自己不能持有該對象,這時會被釋放并被廢棄,是以會引起編譯器警告。
?
1 2 3 4 | |
編譯器如何處理該源代碼呢?
?
1 2 3 4 5 6 7 | |
雖然自己生成并持有的對象通過objc_initWeak 函數被指派給附有_ _ weak 修飾符的變量中,但編譯器判斷其沒有持有者(即 強引用),故該對象立即通過objc_release 函數被釋放和廢棄。
這樣一來,nil 就會被指派給引用廢棄對象的附有_ _ weak 修飾符的變量中。
下面我們通過NSLog 函數來驗證一下。
?
1 2 3 4 | |
以下為該源代碼的輸出結果,其中用%@ 輸出nil。
?
1 | |
關于“立即釋放對象”
如前所述,以下源代碼會引起編譯器警告。
?
1 | |
這是由于編譯器判斷生成并持有的對象不能繼續持有。附有__unsafe_unretained修飾符的變量又如何呢?
?
1 | |
與__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];
^ ~~~~~~~~~~~~~~~~~~~~~~~
該源代碼通過編譯器轉換為以下形式。
?
1 2 3 4 | |
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 中的對象。
?
1 2 3 4 | |
該源代碼可轉換為如下形式:
?
1 2 3 4 5 6 7 | |
與被指派時相比,在使用附有_ _ weak 修飾符變量的情形下,增加了對objc_loadWeakRetained函數和objc_autorelease 函數的調用。這些函數的動作如下。
(1)objc_loadWeakRetained 函數取出附有_ _ weak 修飾符變量所引用的對象并retain。
(2)objc_autorelease 函數将對象注冊到autoreleasepool 中。
由此可知,因為附有_ _ weak 修飾符變量所引用的對象像這樣被注冊到autoreleasepool 中,是以在@autoreleasepool 塊結束之前都可以放心使用。但是,如果大量地使用附有_ _ weak 修飾符的變量,注冊到autoreleasepool 的對象也會大量地增加,是以在使用附有_ _ weak 修飾符的變量時,最好先暫時指派給附有_ _ strong 修飾符的變量後再使用。
比如,以下源代碼使用了5 次附有_ _ weak 修飾符的變量o。
?
1 2 3 4 5 6 7 8 | |
相應地,變量o 所指派的對象也就注冊到autoreleasepool 中5 次。
?
1 2 3 4 5 6 7 8 9 10 11 | |
将附有_ _ w e a k 修飾符的變量o 指派給附有_ _ s t r o n g 修飾符的變量後再使用可以避免此類問題。
?
1 2 3 4 5 6 7 8 9 | |
在“tmp = o;”時對象僅登入到autoreleasepool 中1 次。
?
1 2 3 4 5 6 7 | |
在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”。如以下的源代碼:
?
1 2 3 4 5 6 7 8 9 | |
由于最開始生成并持有的對象為附有__strong修飾符變量obj所持有的強引用,是以在該變量作用域結束之前都始終存在。是以如下所示,在變量作用域結束之前,可以持續使用附有__weak修飾符的變量o所引用的對象。
1 <NSObject: 0x753e180>
2 <NSObject: 0x753e180>
3 <NSObject: 0x753e180>
4 <NSObject: 0x753e180>
5 <NSObject: 0x753e180>
下面對retainWeakReference方法進行試驗。我們做一個MyObject類,讓其繼承NSObject類并實作retainWeakReference方法。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
該例中,當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方法,是以從該方法中再次操作運作時庫時,其操作内容會永久等待。原本這些方法并沒有記入文檔,是以應用程式程式設計人員不可能實作該方法群,但如果因某些原因而不得不實作,那麼還是在全部了解的基礎上實作比較好。