用了block很長時間,也能避免相關(guān)的使用問題,想研究下大體底層實現(xiàn),看了很多的優(yōu)秀博客,這里寫一下自己的理解。
首先block用Apple文檔的話來說,“A block is an anonymous inline collection of code, and sometimes also called a "closure".
個人理解:block就是一個匿名內(nèi)聯(lián)的代碼集合,有時也叫他"closure"<閉包>
閉包是啥:看到網(wǎng)上一句經(jīng)典的描述,閉包就是能夠讀取其他函數(shù)內(nèi)部的函數(shù)。
通常來說,block都是一些簡短代碼片塊的封裝,適用作工作單元,通常用來做并發(fā)任務(wù)、遍歷、以及回調(diào)。
比如:
多線程的相關(guān)的操作,GCD蘋果都是以block的形式,等等
數(shù)組,字典的相關(guān)的遍歷,等等
網(wǎng)絡(luò)請求的相關(guān)回調(diào)之類的,界面跳轉(zhuǎn)傳值,等等
當前也有很多的三方框架也應(yīng)用很多的block,通過block進行異步傳值,進行事件響應(yīng)回調(diào)。
使用 clang -rewrite-objc 來深入了解一下block吧。
clang提供的中間代碼可以帶我們簡單的了解一下block。clang可以將我們寫完的OC代碼編譯成C++代碼。
1、首先進入程序的目錄

2、執(zhí)行clang

3、生成相關(guān)的cpp文件

但是帶有
#import <UIKit/UIKit.h>
的類好像是轉(zhuǎn)不不成cpp文件的。
編譯的結(jié)果內(nèi)容比較多,一個簡單的main.m文件簡單的幾句代碼,編譯之后就大概有10萬多行。
很多語言都可以只實現(xiàn)編譯器前端,生成C中間代碼,然后利用現(xiàn)有的很多C編譯器后端。
通過代碼進行編譯查看
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^myBlock1)() = ^{
printf("hello world1111");
};
myBlock1();
void (^myBlock2)() = ^{
printf("hello world2222");
};
myBlock2();
}
return 0;
}
clang編譯結(jié)果如下
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("hello world1111");
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_1(struct __main_block_impl_1 *__cself) {
printf("hello world2222");
}
static struct __main_block_desc_1 {
size_t reserved;
size_t Block_size;
} __main_block_desc_1_DATA = { 0, sizeof(struct __main_block_impl_1)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock1)->FuncPtr)((__block_impl *)myBlock1);
void (*myBlock2)() = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock2)->FuncPtr)((__block_impl *)myBlock2);
}
return 0;
}
<1>、首先出現(xiàn)的結(jié)構(gòu)體就是__main_block_impl_0,可以看出是根據(jù)所在函數(shù)(main函數(shù))以及出現(xiàn)序列(第0個)進行命名的。
下面還有一個__main_block_impl_1,應(yīng)該是按照出現(xiàn)序列的(第1個)進行命名的。
<2>、__main_block_impl_0中包含了兩個成員變量和一個構(gòu)造函數(shù),成員變量分別是__block_impl結(jié)構(gòu)體和描述信息Desc,之后在構(gòu)造函數(shù)中初始化block的類型信息和函數(shù)指針等信息。從impl.isa = &_NSConcreteStackBlock;
<3>、接著出現(xiàn)的是 __main_block_func_0 函數(shù),即block對應(yīng)的函數(shù)體。該函數(shù)接受一個__cself參數(shù),即對應(yīng)的block自身。
再下面__main_block_desc_0描述的是block的相關(guān)信息,大小。
<4>、最下面展示的就是block的相關(guān)的調(diào)用和實現(xiàn)了。
void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock1)->FuncPtr)((__block_impl *)myBlock1);
void (*myBlock2)() = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock2)->FuncPtr)((__block_impl *)myBlock2);
執(zhí)行的時候?qū)嶋H上就是把block的相關(guān)信息傳了進去,也就是上面介紹的
__main_block_impl_0
__main_block_func_0
__main_block_desc_0
void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,
&__main_block_desc_0_DATA));
以上是一個block的基本。
block可以訪問局部變量,甚至可以修改局部變量,那么我們來看一下是怎么實現(xiàn)的。
先看一個直接訪問局部變量的示例
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int val = 0;
void (^myBlock1)() = ^{
printf("val === %d",val);
};
myBlock1();
}
return 0;
}
clang -rewrite-objc main.m 之后得到的轉(zhuǎ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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy
printf("val === %d",val);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int val = 0;
void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
((void (*)(__block_impl *))((__block_impl *)myBlock1)->FuncPtr)((__block_impl *)myBlock1);
}
return 0;
}
可以看到__main_block_impl_0中多了一個變量,val。
在實現(xiàn)中__main_block_func_0多了一個
int val = __cself->val; // bound by copy
__cself->val,應(yīng)該是類似于self.val。直接把val作為一個屬性來進行操作。 bound by copy這個注釋標識代表了,val是被拷貝過來的,內(nèi)存地址和上面的val 是不一樣的。
int val = 0;
printf(" \nblock外面 %p",&val);
void (^myBlock1)() = ^{
printf("\nval === %d",val);
printf("\nblock里面 %p",&val);
};
myBlock1();
printf("\nblock外面 %p\n",&val);
打印的結(jié)果
block外面 0x7fff5fbff78c
val === 0
block里面 0x1005000f0
block外面 0x7fff5fbff78c
里外的val的對應(yīng)的內(nèi)存地址已經(jīng)發(fā)生變化了,但是當block調(diào)用完畢之后,val的地址沒有發(fā)生變化,也就是說,在block里面使用val的時候復(fù)制了一個新的地址進行使用了。也就能理解在block內(nèi)部修改val是修改不了的,因為val在block的地址和外面的不一樣,里面是一個臨時copy的一個全新地址的參數(shù)。
但是想直接修改局部變量的時候報錯了,提示要用__block來進行修飾val。

那么,為什么這個時候不能給val進行賦值呢?
摘自網(wǎng)上的一段解釋,理解一下。
因為main函數(shù)中的局部變量val和函數(shù)__main_block_func_0不在同一個作用域中,調(diào)用過程中只是進行了值傳遞。當然,在上面代碼中,我們可以通過指針來實現(xiàn)局部變量的修改。不過這是由于在調(diào)用__main_block_func_0時,main函數(shù)棧還沒展開完成,變量val還在棧中。但是在很多情況下,block是作為參數(shù)傳遞以供后續(xù)回調(diào)執(zhí)行的。通常在這些情況下,block被執(zhí)行時,定義時所在的函數(shù)棧已經(jīng)被展開,局部變量已經(jīng)不在棧中了(block此時在哪里?),再用指針訪問就……
當時看完了有幾個比較疑惑的點。
1、"main函數(shù)中的局部變量val和函數(shù)__main_block_func_0不在同一個作用域中"。為什么不在一個作用域。
val 的作用域是main函數(shù),__main_block_func_0是全局的
static void __main_block_func_0
通過這個可以看出。
val只是一個局部變量
2、"main函數(shù)棧還沒展開完成",這句話啥意思?展開什么意思?
展開就是釋放的意思,就是說,main函數(shù)被釋放了。
3、"當然,在上面代碼中,我們可以通過指針來實現(xiàn)局部變量的修改",這個怎么實現(xiàn)
int main(int argc, const char * argv[]) {
@autoreleasepool {
int val = 0;
int *bbb = &val;
printf(" \nblock外面 %p val = %d",&val,val);
void (^myBlock1)() = ^{
*bbb = 30;
printf("\nblock里面 %p val = %d",&val,val);
};
printf("\nblock外面111 %p val = %d",&val,val);
myBlock1();
printf("\nblock外面222 %p val = %d\n",&val,val);
}
return 0;
}
打印的結(jié)果
block外面 0x7fff5fbff76c val = 0
block外面111 0x7fff5fbff76c val = 0
block里面 0x100107508 val = 0
block外面222 0x7fff5fbff76c val = 30
最終的結(jié)果可以看到 val的地址沒有發(fā)生變化,除了在block里面<block里面 0x100107508 val = 0>,這個打印的其實不是真正的val,是block copy的val,所以還是0。
修改了val的值,并且沒有修改val的地址。
來看一下我們熟悉的解決方法
添加__block修飾val對象。并且探索一下,__block怎么實現(xiàn)的。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int val = 0;
printf(" \nblock外面 %p val = %d",&val,val);
void (^myBlock1)() = ^{
printf("\nblock里面 %p val = %d",&val,val);
};
printf("\nblock外面111 %p val = %d",&val,val);
myBlock1();
printf("\nblock外面222 %p val = %d\n",&val,val);
}
return 0;
}
打印結(jié)果,內(nèi)存地址徹底的被修改了。執(zhí)行void (^myBlock1)()的時候就被修改了
block外面 0x7fff5fbff738 val = 0
block外面111 0x100102088 val = 0
block里面 0x100102088 val = 0
block外面222 0x100102088 val = 0
通過clang看一下情況
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; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
printf("\nblock里面 %p val = %d",&(val->__forwarding->val),(val->__forwarding->val));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};
printf(" \nblock外面 %p val = %d",&(val.__forwarding->val),(val.__forwarding->val));
void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
printf("\nblock外面111 %p val = %d",&(val.__forwarding->val),(val.__forwarding->val));
((void (*)(__block_impl *))((__block_impl *)myBlock1)->FuncPtr)((__block_impl *)myBlock1);
printf("\nblock外面222 %p val = %d\n",&(val.__forwarding->val),(val.__forwarding->val));
}
return 0;
}
來看一下和val在沒有添加__block修飾的時候的clang出來的代碼的主要區(qū)別
1、由第一個成員__isa指針也可以知道** __Block_byref_val_0也可以是NSObject。
第二個成員__forwarding指向自己,為什么要指向自己?指向自己是沒有意義的,只能說有時候需要指向另一個__Block_byref_val_0結(jié)構(gòu)。后面我們揭曉__forwarding**。
最后一個成員是目標存儲變量val。
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
2、__main_block_impl_0中也發(fā)生了變化
int val;變成了現(xiàn)在的*__Block_byref_val_0 val; // by ref
3、** __main_block_func_0中和之前也不一樣了
int val = __cself->val; // bound by copy
變成了現(xiàn)在的
__Block_byref_val_0 val = __cself->val; // bound by ref
__Block_byref_val_0指針類型變量val通過其成員變量__forwarding*指針來操作另一個成員變量。
4、** __main_block_desc_0**中多了兩個東西
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
從上面的clang編譯出來的代碼可以看到,block被轉(zhuǎn)化成了__main_block_impl_0結(jié)構(gòu)體實例,該實例持有__Block_byref_val_0結(jié)構(gòu)體實例的指針。
看一下 val 在block中的調(diào)用的情況
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
printf("\nblock里面 %p val = %d",&(val->__forwarding->val),(val->__forwarding->val));
}
通過__cself找到結(jié)構(gòu)體__Block_byref_val_0,然后通過__forwarding找到結(jié)構(gòu)體的成員val。成員變量val是該實例自身持有的變量,指向的是原來的局部變量。如圖所示:
<a >圖片來自</a>

但是還是存在問題
<1>、__Block_byref_val_0類型的變量對應(yīng)的val仍然在棧上,當block執(zhí)行回調(diào)的時候,val所對用的棧被釋放了怎么辦?
<2>、為什么訪問val還要通過__forwarding?不直接修修改或者訪問val呢?
存儲域
上面的clang出的代碼可以看出。 isa指向的是 _NSConcreteStackBlock,還有另外的兩個類似的
_NSConcreteStackBlock 保存在棧中的block,出棧時會被銷毀
_NSConcreteGlobalBlock 全局的靜態(tài)block,不會訪問任何外部變量
_NSConcreteMallocBlock 保存在堆中的block,當引用計數(shù)為0時會被銷毀
上面我們的代碼,blcok是在棧上生成的,現(xiàn)在創(chuàng)建一個_NSConcreteGlobalBlock類型的block
#import <Foundation/Foundation.h>
void (^myBlock)()=^{
printf("block");
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
myBlock();
}
return 0;
}
clang -rewrite-objc main.m之后的代碼是
struct __myBlock_block_impl_0 {
struct __block_impl impl;
struct __myBlock_block_desc_0* Desc;
__myBlock_block_impl_0(void *fp, struct __myBlock_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __myBlock_block_func_0(struct __myBlock_block_impl_0 *__cself) {
printf("block");
}
static struct __myBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
} __myBlock_block_desc_0_DATA = { 0, sizeof(struct __myBlock_block_impl_0)};
static __myBlock_block_impl_0 __global_myBlock_block_impl_0((void *)__myBlock_block_func_0, &__myBlock_block_desc_0_DATA);
void (*myBlock)()=((void (*)())&__global_myBlock_block_impl_0);
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}
return 0;
}
可以看到
impl.isa = &_NSConcreteGlobalBlock;
分配在全局的block,當超出變量的作用域的時候,依然可以通過指針進行安全的訪問。但是在棧上面的block,如果他所屬于的變量的作用域結(jié)束,那么該block的作用域結(jié)束也就結(jié)束了。同樣的__block修飾的變量也分配在棧上,當超過該變量的作用域時,該__block修飾的變量也會被廢棄。
這個時候就需要一個在堆上面block,生命周期由我們自己控制。
_NSConcreteMallocBlock登場。
這個時候就會將block和__block修飾的變量,從棧上復(fù)制到堆上面。那么,棧上的block就可以超出所屬的變量的作用域了,那么復(fù)制到堆上面的block以及__block所修飾的變量仍然可以進行操作。
復(fù)制到堆上面的block也就變成
impl.isa = &_NSConcreteMallocBlock;
而此時 結(jié)構(gòu)體中的__forwarding就發(fā)揮了作用,保證能訪問從棧上拷貝到堆上的__block修飾的變量。
我們一般可以使用copy方法手動將 Block 或者 __block變量從棧復(fù)制到堆上。比如我們把Block做為類的屬性訪問時,我們一般把該屬性設(shè)為copy。有些情況下我們可以不用手動復(fù)制,比如Cocoa框架中使用含有usingBlock方法名的方法時,或者GCD的API中傳遞Block時。
當一個Block被復(fù)制到堆上時,與之相關(guān)的__block變量也會被復(fù)制到堆上,此時堆上的Block持有相應(yīng)堆上的__block變量。當堆上的__block變量沒有持有者時,它才會被廢棄。(這里的思考方式和objc引用計數(shù)內(nèi)存管理完全相同。)
當棧上的__block修飾的變量被復(fù)制到了堆上之后,那么之后訪問堆上的變量就通過val->__forwarding->val了。
讓我們來看一下上面的第4點不同,多了的那兩句話,此時main_block_desc_0多了兩個成員函數(shù),分別是** copy和 dispose分別指向__main_block_copy__0和__main_block_dispose__0**
當block從棧上被拷貝到堆上的時候,會調(diào)用__main_block_copy_0將__block類型的成員變量val從棧上復(fù)制到堆上;而當block被釋放時,相應(yīng)地會調(diào)用__main_block_dispose_0來釋放__block類型的成員變量val。
這時候,__forwarding的作用就體現(xiàn)出來了:當一個__block變量從棧上被復(fù)制到堆上時,棧上的那個__Block_byref_val_0結(jié)構(gòu)體中的__forwarding指針也會指向堆上的結(jié)構(gòu)。
<a >圖片來自</a>

__block可以指定任何的局部變量,上面的代碼有如下代碼
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
當block從棧上復(fù)制到堆上面的時候,會使用__main_block_copy該方法持有變量(相當于retain),當堆上面的block被廢棄的時候,就會使用__main_block_dispose釋放__block修飾的變量(相當于release)。
我們一開始用的demo,block一直實在棧上的,聲明的局部變量也是在棧上的,他們都在一個方法中,生命周期的話都依據(jù)所持有的方法。方法釋放了,局部變量和block也玩兒完,但是為什么這個時候局部變量想在block中修改還是必須得添加__block呢?
猜想,可能這是蘋果指定的規(guī)則,block是作為參數(shù)傳遞以供后續(xù)回調(diào)執(zhí)行的,block被傳遞出去了,局部變量持有者可能因為沒啥用了就被釋放了,那么局部變量也就被釋放了,再在block中修改局部變量就危險了。所以,不管在什么時候,blcok中修改局部變量都得添加__block來進行修飾。
理論部分說完了,來玩一下斷點看一下情況吧。

不對啊,和之前說好的不一樣啊,不應(yīng)該是在棧上的嗎?
不應(yīng)該是clang以后的_NSConcreteStackBlock嗎?難道上面的理論都不成立?網(wǎng)上肯定不只有我瞎扯。
看到大神的博客,安心了。
http://blog.devtang.com/2013/07/28/a-look-inside-blocks/#NSConcreteGlobalBlock__u7C7B_u578B_u7684_block__u7684_u5B9E_u73B0
在 ARC 開啟的情況下,將只會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 類型的 block。
原本的 NSConcreteStackBlock 的 block 會被 NSConcreteMallocBlock 類型的 block 替代。證明方式是以下代碼在 XCode 中,會輸出 <NSMallocBlock: 0x100109960>
。在蘋果的 官方文檔 中也提到,當把棧中的 block 返回時,不需要調(diào)用 copy 方法了。
由于 ARC 已經(jīng)能很好地處理對象的生命周期的管理,這樣所有對象都放到堆上管理,對于編譯器實現(xiàn)來說,會比較方便。
如有失誤請各位路過大神即時指點,或有更好的做法,也請指點一二,在下感激不盡。
參考的網(wǎng)址:
http://www.cocoachina.com/ios/20150106/10850.html
http://blog.devtang.com/2013/07/28/a-look-inside-blocks/
http://blog.csdn.net/jasonblog/article/details/7756763
http://blog.csdn.net/hherima/article/details/38620175
http://www.dreamingwish.com/articlelist/category/toturial