將我認(rèn)為的比較易懂的關(guān)于block的文章整理到一起:
文章鏈接:
在本文的開頭,提出兩個(gè)簡(jiǎn)單的問題,如果你不能從根本上弄懂這兩個(gè)問題,那么希望你閱讀完本文后能有所收獲。
為什么block中不能修改普通變量的值?
__block的作用就是讓變量的值在block中可以修改么?
如果有的讀者認(rèn)為,問題太簡(jiǎn)單了,而且你的答案是:
因?yàn)榫幾g器會(huì)有警告,各種教程也都說了不能修改。
應(yīng)該是的吧。
那么我也建議你,抽出寶貴的幾分鐘時(shí)間閱讀完本文吧。在開始揭開__block的神秘面紗之前,很不幸的是我們需要重新思考一下block的本質(zhì)和它的實(shí)現(xiàn)。
block是什么?
很多教程、資料上都稱Block是“帶有自動(dòng)變量值的匿名函數(shù)”。這樣的解釋顯然是正確的,但也是不利于初學(xué)者理解的。我們首先通過一個(gè)例子看一看block到底是什么?
typedef void (^Block)(void);
Block block;
{???
?????????int val = 0;??
????????block = ^(){?????????
???????????????NSLog(@"val = %d",val);?
????????};
}
block();
拋開block略有怪異的語法不談,其實(shí)對(duì)于一個(gè)block來說:
它更像是一個(gè)微型的程序。
為什么這么說呢,我們知道程序就是數(shù)據(jù)加上算法,顯然,block有著自己的數(shù)據(jù)和算法。可以看到,在這個(gè)簡(jiǎn)單的例子中,block的數(shù)據(jù)就是int類型變量val,它的算法就是一個(gè)簡(jiǎn)單的NSLog方法。對(duì)于一般的block來說,它的數(shù)據(jù)就是傳入的參數(shù)和在定義這個(gè)block時(shí)截獲的變量。而它的算法,就是我們往里面寫的那些方法、函數(shù)調(diào)用等。
我認(rèn)為block像是一個(gè)微型程序的另一個(gè)主要原因是一個(gè)block對(duì)象可以由程序員選擇在什么時(shí)候調(diào)用。比如,如果我喜歡,我可以設(shè)置一個(gè)定時(shí)器,在10s后執(zhí)行這個(gè)block,或者在另一個(gè)類里執(zhí)行這個(gè)block。
當(dāng)然,我們還注意到在上面的demo中,通過typedef,block非常類似于一個(gè)OC的對(duì)象。限于篇幅和主題,這里不加證明的給出一個(gè)結(jié)論:Block其實(shí)就是一個(gè)Objective-C的對(duì)象。有興趣的讀者可以結(jié)合runtime中類和對(duì)象的定義進(jìn)一步思考。
block是怎么實(shí)現(xiàn)的?
剛剛我們已經(jīng)意識(shí)到,block的定義和調(diào)用是分離的。通過clang編譯器,可以看到block和其他Objective-C對(duì)象一樣,都是被編譯為C語言里的普通的struct結(jié)構(gòu)體來實(shí)現(xiàn)的。我們來看一個(gè)最簡(jiǎn)單的block會(huì)被編譯成什么樣:
//這個(gè)是源代碼
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)
};
代碼非常長(zhǎng),但是并不復(fù)雜,一共是四個(gè)結(jié)構(gòu)體,顯然一個(gè)block對(duì)象被編譯為了一個(gè)__main_block_impl_0類型的結(jié)構(gòu)體。這個(gè)結(jié)構(gòu)體由兩個(gè)成員結(jié)構(gòu)體和一個(gè)構(gòu)造函數(shù)組成。兩個(gè)結(jié)構(gòu)體分別是__block_impl和__main_block_desc_0類型的。其中__block_impl結(jié)構(gòu)體中有一個(gè)函數(shù)指針,指針將指向__main_block_func_0類型的結(jié)構(gòu)體。總結(jié)了一副關(guān)系圖:
block在定義的時(shí)候:
//調(diào)用__main_block_impl_0結(jié)構(gòu)體的構(gòu)造函數(shù)
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在調(diào)用的時(shí)候:
(*blk->impl.FuncPtr)(blk);
之前我們說到,block有自己的數(shù)據(jù)和算法。顯然算法(也就是代碼)是放在__main_block_func_0結(jié)構(gòu)體里的。那么數(shù)據(jù)在哪里呢,這個(gè)問題比較復(fù)雜,我們來看一看文章最初的demo會(huì)編譯成什么樣,為了簡(jiǎn)化代碼,這里只貼出需要修改的部分。
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);
}
可以看到,當(dāng)block需要截獲自動(dòng)變量的時(shí)候,首先會(huì)在__main_block_impl_0結(jié)構(gòu)體中增加一個(gè)成員變量并且在結(jié)構(gòu)體的構(gòu)造函數(shù)中對(duì)變量賦值。以上這些對(duì)應(yīng)著block對(duì)象的定義。
在block被執(zhí)行的時(shí)候,把__main_block_impl_0結(jié)構(gòu)體,也就是block對(duì)象作為參數(shù)傳入__main_block_func_0結(jié)構(gòu)體中,取出其中的val的值,進(jìn)行接下來的操作。
為什么__block中不能修改變量值?
如果你耐心地看完了上面非常啰嗦繁瑣的block介紹,那么你很快就明白為什么block中不能修改普通的變量的值了。
通過把block拆成這四個(gè)結(jié)構(gòu)體,系統(tǒng)“完美”的實(shí)現(xiàn)了一個(gè)block,使得它可以截獲自動(dòng)變量,也可以像一個(gè)微型程序一樣,在任意時(shí)刻都可以被調(diào)用。但是,block還存在這一個(gè)致命的不足:
注意到之前的__main_block_func_0結(jié)構(gòu)體,里面有printf方法,用到了變量val,但是這個(gè)block,和最初block截獲的block,除了數(shù)值一樣,再也沒有一樣的地方了。參見這句代碼:
int val = __cself->val;
當(dāng)然這并沒有什么影響,甚至還有好處,因?yàn)閕nt val變量定義在棧上,在block調(diào)用時(shí)其實(shí)已經(jīng)被銷毀,但是我們還可以正常訪問這個(gè)變量。但是試想一下,如果我希望在block中修改變量的值,那么受到影響的是int val而非__cself->val,事實(shí)上即使是__cself->val,也只是截獲的自動(dòng)變量的副本,要想修改在block定義之外的自動(dòng)變量,是不可能的事情。這就是為什么我把demo略作修改,增加一行代碼,但是輸出結(jié)果依然是”val = 0”。
//修改后的demo
typedef ?void ?(^Block)(void);
Block block;
{
????????int val = 0;
????????block = ^(){
????????????????NSLog(@"val = %d",val);
????????};
????????val = 1;
}
block();
既然無法實(shí)現(xiàn)修改截獲的自動(dòng)變量,那么編譯器干脆就禁止程序員這么做了。
__block修飾符是如何做到修改變量值的
如果把val變量加上__block修飾符,編譯器會(huì)怎么做呢?
//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);
}
改動(dòng)并不大,簡(jiǎn)單來說,只是把val封裝在了一個(gè)結(jié)構(gòu)體中而已。可以用下面這個(gè)圖來表示五個(gè)結(jié)構(gòu)體之間的關(guān)系。
但是關(guān)鍵在于__main_block_impl_0結(jié)構(gòu)體中的這一行:
__Block_byref_val_0 *val;
由于__main_block_impl_0結(jié)構(gòu)體中現(xiàn)在保存了一個(gè)指針變量,所以任何對(duì)這個(gè)指針的操作,是可以影響到原來的變量的。
進(jìn)一步,我們考慮截獲的自動(dòng)變量是Objective-C的對(duì)象的情況。在開啟ARC的情況下,將會(huì)強(qiáng)引用這個(gè)對(duì)象一次。這也保證了原對(duì)象不被銷毀,但與此同時(shí),也會(huì)導(dǎo)致循環(huán)引用問題。
需要注意的是,在未開啟ARC的情況下,如果變量附有__block修飾符,將不會(huì)被retain,因此反而可以避免循環(huán)引用的問題。
回到開篇的兩個(gè)問題,答案應(yīng)該很明顯了。
1.由于無法直接獲得原變量,技術(shù)上無法實(shí)現(xiàn)修改,所以編譯器直接禁止了。
2.都可以用來讓變量在block中可以修改,但是在非ARC模式下,__block修飾符會(huì)避免循環(huán)引用。注意:block的循環(huán)引用并非__block修飾符引起,而是由其本身的特性引起的。
其他文章補(bǔ)充:
1.
Blocks可以訪問局部變量,但是不能修改
如果修改局部變量,需要加__block。
聲明block的時(shí)候?qū)嶋H上是把當(dāng)時(shí)的臨時(shí)變量又復(fù)制了一份,在block里即使修改了這些復(fù)制的變量,也不影響外面的原始變量。即所謂的閉包。但是當(dāng)變量是一個(gè)指針的時(shí)候,block里只是復(fù)制了一份這個(gè)指針,兩個(gè)指針指向同一個(gè)地址。所以,在block里面對(duì)指針指向內(nèi)容做的修改,在block外面也一樣生效。
2.
在ARC的環(huán)境里面,默認(rèn)情況下當(dāng)你在block里面引用一個(gè)Objective-C對(duì)象的時(shí)候,該對(duì)象會(huì)被retain。當(dāng)你簡(jiǎn)單的引用了一個(gè)對(duì)象的實(shí)例變量時(shí),它同樣被retain。但是被__block存儲(chǔ)類型修飾符標(biāo)記的對(duì)象變量不會(huì)被retain。
而在非ARC下,則不會(huì)retain這個(gè)對(duì)象,也不會(huì)導(dǎo)致循環(huán)引用。
3.
在垃圾回收機(jī)制里面,如果你同時(shí)使用__weak和__block來標(biāo)識(shí)一個(gè)變量,那么該block將不會(huì)保證它是一直是有效的。?
4.__block和__weak修飾符的區(qū)別其實(shí)是挺明顯的:
//使用了__weak修飾符的對(duì)象,作用等同于定義為weak的property。當(dāng)原對(duì)象沒有任何強(qiáng)引用的時(shí)候,弱引用指針也會(huì)被設(shè)置為nil。
//__block對(duì)象在block中是可以被修改、重新賦值的。
1)__block不管是ARC還是MRC模式下都可以使用,可以修飾對(duì)象,還可以修飾基本數(shù)據(jù)類型。
2)__weak只能在ARC模式下使用,也只能修飾對(duì)象(NSString),不能修飾基本數(shù)據(jù)類型(int)。
3)__block對(duì)象可以在block中被重新賦值,__weak不可以。
4)__block對(duì)象在ARC下可能會(huì)導(dǎo)致循環(huán)引用,非ARC下會(huì)避免循環(huán)引用,__weak只在ARC下使用,可以避免循環(huán)引用。