API Reference對__block變量修飾符有如下幾處解釋:
//A powerful feature of blocks is that they can modify variables in the same lexical scope. You signal that a block can modify a variable using the __block storage type modifier. //At function level are __block variables. These are mutable within the block (and the enclosing scope) and are preserved if any referencing block is copied to the heap.
大概意思歸結出來就是:
__block對象在block中是可以被修改、重新賦值的。
API Reference對__weak變量修飾符有如下幾處解釋:
__weak specifies a reference that does not keep the referenced object alive. A weak reference is set to nil when there are no strong references to the object.
使用了__weak修飾符的對象,作用等同於定義為weak的property。自然不會導致循環引用問題,因為蘋果文檔已經說的很清楚,當原對象沒有任何強引用的時候,弱引用指針也會被設置為nil。
因此,__block和__weak修飾符的區別其實是挺明顯的:
1.__block不管是ARC還是MRC模式下都可以使用,可以修飾對象,還可以修飾基本數據類型。
2.__weak只能在ARC模式下使用,也只能修飾對象(NSString),不能修飾基本數據類型(int)。
3.__block對象可以在block中被重新賦值,__weak不可以。
4.__block對象在ARC下可能會導致循環引用,非ARC下會避免循環引用,__weak只在ARC下使用,可以避免循環引用。
PS:__unsafe_unretained修飾符可以被視為iOS SDK 4.3以前版本的__weak的替代品,不過不會被自動置空為nil。所以盡可能不要使用這個修飾符。
更多__block的解釋
在本文的開頭,提出兩個簡單的問題,如果你不能從根本上弄懂這兩個問題,那么希望你閱讀完本文后能有所收獲。
- 為什么block中不能修改普通變量的值?
- __block的作用就是讓變量的值在block中可以修改么?
如果有的讀者認為,問題太簡單了,而且你的答案是:
- 因為編譯器會有警告,各種教程也都說了不能修改。
- 應該是的吧。
那么我也建議你,抽出寶貴的幾分鍾時間閱讀完本文吧。在開始揭開__block的神秘面紗之前,很不幸的是我們需要重新思考一下block的本質和它的實現。
block是什么?
很多教程、資料上都稱Block是“帶有自動變量值的匿名函數”。這樣的解釋顯然是正確的,但也是不利於初學者理解的。我們首先通過一個例子看一看block到底是什么?
typedef void (^Block)(void); Block block; { int val = 0; block = ^(){ NSLog(@"val = %d",val); }; } block();
拋開block略有怪異的語法不談,其實對於一個block來說:
它更像是一個微型的程序。
為什么這么說呢,我們知道程序就是數據加上算法,顯然,block有着自己的數據和算法。可以看到,在這個簡單的例子中,block的數據就是int類型變量val,它的算法就是一個簡單的NSLog方法。對於一般的block來說,它的數據就是傳入的參數和在定義這個block時截獲的變量。而它的算法,就是我們往里面寫的那些方法、函數調用等。
我認為block像是一個微型程序的另一個主要原因是一個block對象可以由程序員選擇在什么時候調用。比如,如果我喜歡,我可以設置一個定時器,在10s后執行這個block,或者在另一個類里執行這個block。
當然,我們還注意到在上面的demo中,通過typedef,block非常類似於一個OC的對象。限於篇幅和主題,這里不加證明的給出一個結論:Block其實就是一個Objective-C的對象。有興趣的讀者可以結合runtime中類和對象的定義進一步思考。
block是怎么實現的?
剛剛我們已經意識到,block的定義和調用是分離的。通過clang編譯器,可以看到block和其他Objective-C對象一樣,都是被編譯為C語言里的普通的struct結構體來實現的。我們來看一個最簡單的block會被編譯成什么樣:
//這個是源代碼 int main(){ void (^blk)(void) = ^{printf("Block\n");}; block(); return 0; }
編譯后的代碼如下:
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 *Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,int flags=0){ impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; struct void __main_block_func_0(struct __main_block_impl_0 *__cself){ printf("Block\n"); } static struct __main_block_desc_0{ unsigned long reserved; unsigned long Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) };
代碼非常長,但是並不復雜,一共是四個結構體,顯然一個block對象被編譯為了一個__main_block_impl_0類型的結構體。這個結構體由兩個成員結構體和一個構造函數組成。兩個結構體分別是__block_impl和__main_block_desc_0類型的。其中__block_impl結構體中有一個函數指針,指針將指向__main_block_func_0類型的結構體。總結了一副關系圖:
block在定義的時候:
//調用__main_block_impl_0結構體的構造函數 struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA); struct __main_block_impl_0 *blk = &tmp;
block在調用的時候:
(*blk->impl.FuncPtr)(blk);
之前我們說到,block有自己的數據和算法。顯然算法(也就是代碼)是放在__main_block_func_0結構體里的。那么數據在哪里呢,這個問題比較復雜,我們來看一看文章最初的demo會編譯成什么樣,為了簡化代碼,這里只貼出需要修改的部分。
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 *Desc; int val; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,int _val, int flags=0) : val(_val){ impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; struct void __main_block_func_0(struct __main_block_impl_0 *__cself){ int val = __cself->val; printf("val = %d",val); }
可以看到,當block需要截獲自動變量的時候,首先會在__main_block_impl_0結構體中增加一個成員變量並且在結構體的構造函數中對變量賦值。以上這些對應着block對象的定義。
在block被執行的時候,把__main_block_impl_0結構體,也就是block對象作為參數傳入__main_block_func_0結構體中,取出其中的val的值,進行接下來的操作。
為什么__block中不能修改變量值?
如果你耐心地看完了上面非常啰嗦繁瑣的block介紹,那么你很快就明白為什么block中不能修改普通的變量的值了。
通過把block拆成這四個結構體,系統“完美”的實現了一個block,使得它可以截獲自動變量,也可以像一個微型程序一樣,在任意時刻都可以被調用。但是,block還存在這一個致命的不足:
注意到之前的__main_block_func_0結構體,里面有printf方法,用到了變量val,但是這個block,和最初block截獲的block,除了數值一樣,再也沒有一樣的地方了。參見這句代碼:
int val = __cself->val;
當然這並沒有什么影響,甚至還有好處,因為int val變量定義在棧上,在block調用時其實已經被銷毀,但是我們還可以正常訪問這個變量。但是試想一下,如果我希望在block中修改變量的值,那么受到影響的是int val而非__cself->val,事實上即使是__cself->val,也只是截獲的自動變量的副本,要想修改在block定義之外的自動變量,是不可能的事情。這就是為什么我把demo略作修改,增加一行代碼,但是輸出結果依然是”val = 0”。
//修改后的demo typedef void (^Block)(void); Block block; { int val = 0; block = ^(){ NSLog(@"val = %d",val); }; val = 1; } block();
既然無法實現修改截獲的自動變量,那么編譯器干脆就禁止程序員這么做了。
__block修飾符是如何做到修改變量值的
如果把val變量加上__block修飾符,編譯器會怎么做呢?
//int val = 0; 原代碼 __block int val = 0;//修改后的代碼
編譯后的代碼:
struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *forwarding; int __flags; int __size; int val; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 *Desc; __Block_byref_val_0 *val; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,__Block_byref_val_0 *_val, int flags=0) : val(_val->__forwrding){ impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; struct void __main_block_func_0(struct __main_block_impl_0 *__cself){ __Block_byref_val_0 *val = __cself->val; printf("val = %d",val->__forwarding->val); }
改動並不大,簡單來說,只是把val封裝在了一個結構體中而已。可以用下面這個圖來表示五個結構體之間的關系。
但是關鍵在於__main_block_impl_0結構體中的這一行:
__Block_byref_val_0 *val;
由於__main_block_impl_0結構體中現在保存了一個指針變量,所以任何對這個指針的操作,是可以影響到原來的變量的。
進一步,我們考慮截獲的自動變量是Objective-C的對象的情況。在開啟ARC的情況下,將會強引用這個對象一次。這也保證了原對象不被銷毀,但與此同時,也會導致循環引用問題。
需要注意的是,在未開啟ARC的情況下,如果變量附有__block修飾符,將不會被retain,因此反而可以避免循環引用的問題。
總結
回到上面的兩個問題,答案應該很明顯了。
- 由於無法直接獲得原變量,技術上無法實現修改,所以編譯器直接禁止了。
- 都可以用來讓變量在block中可以修改,但是在非ARC模式下,__block修飾符會避免循環引用。注意:block的循環引用並非__block修飾符引起,而是由其本身的特性引起的。