Block

將我認(rèn)為的比較易懂的關(guān)于block的文章整理到一起:

文章鏈接:

你真的理解__block修飾符的原理么?

__block 與 __weak的區(qū)別理解

在本文的開頭,提出兩個(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)引用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容