
說起Block,我想大家再熟悉不過了。每天的開發(fā)過程中都在與他打著交道,簡潔高效的使用給我們的工作帶來了便利。
正是因?yàn)?strong>Block對于我們而言不可或缺,我更覺得應(yīng)該抽出時(shí)間好好認(rèn)識一下Block,了解一些他的優(yōu)缺點(diǎn)、過往經(jīng)歷,讓我們對它的理解更深一層,進(jìn)而開發(fā)出更加高效的程序。
對Block的認(rèn)識
Block與C函數(shù)的區(qū)別和聯(lián)系
Block是C語言的擴(kuò)充,常被稱為帶有局部變量的匿名函數(shù);
看一下正規(guī)的C語言函數(shù)定義:
int test(int count);
int result = test(10);
以上聲明了函數(shù)名為test函數(shù);
使用函數(shù)指針調(diào)用函數(shù):
int (*testFuncPtr)(int) = &test;
int result = (* testFuncPtr)(10);
對比Block,Block就可以定義為不帶名稱的函數(shù),說的直白一點(diǎn),就是匿名函數(shù),即不用顯式的定義函數(shù)名稱,但是匿名函數(shù)都有固定的一個(gè)表達(dá)式。
比如:
^(int count){
printf("%d", count);
}
等價(jià)于:
void test(int count){
}
Block語法定義格式如下:
^ 返回值類型 參數(shù)列表 表達(dá)式
省略之后:
^ 表達(dá)式
通過上面的例子可以看出兩點(diǎn)不同:
- 沒有函數(shù)名稱;
- 帶有“^”符號;
Block的基本使用
可以通過定義Block類型的變量對Block進(jìn)行調(diào)用,與函數(shù)指針有相似之處,具體如下:
int (^block)(int) = ^(int count){
return count + 1;
}
block(5);

Block類型變量用途:局部變量、參數(shù)、靜態(tài)變量、全局變量以及靜態(tài)全局變量等等。
Block 可以作為參數(shù)進(jìn)行傳遞,也可以作為返回值進(jìn)行傳遞,具體如下:
- (void)testFuncWithBlock:(void (^)(int count))block{
if (block) {
block(5);
}
}
// 將Block作為返回值,代碼摘自·Masonry·
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
具體以Masonry使用為例講解一下,Block調(diào)用:
// Masonry 在常用布局時(shí)用到方法如下:
make.top.equalTo(self.titleLabel.mas_bottom).offset(15);
// 有些同學(xué)可能對上述的鏈?zhǔn)秸{(diào)用不是很清楚,其實(shí)上述鏈?zhǔn)秸{(diào)用就是使用Block實(shí)現(xiàn)的,分解如下:
MASConstraint *constraint = make.top;
MASConstraint *(^equalBlk)(id) = constraint.equalTo;
constraint = equalBlk(self.titleLabel.mas_bottom);
MASConstraint *(^offsetBlk)(CGFloat) = constraint.offset;
constraint = offsetBlk(15);
可使用關(guān)鍵字typedef,進(jìn)行Block類型變量的定義
typedef int(^Blk)(int);
// 調(diào)用
Blk blk = ^int (int count){
return count ++;
};
int result = blk(5);
Block 作為屬性變量的用法:
@property (nonatomic, copy) void (^successCompletion)(int);
@property (nonatomic, copy) Blk failCompletion;
通過上面一小節(jié)了解到:
- 函數(shù)與Block區(qū)別與聯(lián)系;
- Block變量和Block表達(dá)式的聯(lián)系
Block的實(shí)質(zhì)
在前面小節(jié)中,介紹了Block的基本用法。那么問題來了,Block究竟是什么呢?現(xiàn)在公認(rèn)的說法是帶有自動(dòng)變量的匿名函數(shù),動(dòng)手一探究竟。
使用Clang -rewrite-objc命令對下面代碼進(jìn)行編譯:
void testBlock(){
int (^blk)(int) = ^(int count){
return count ++;
};
blk(5);
}
執(zhí)行完命令會(huì)在同級目錄下生成XX.cpp文件,這個(gè)文件就是編譯好的文件。
由于編譯后文件包含很多C函數(shù)聲明,在此就不一一解釋了,主要關(guān)注上述Block方法實(shí)現(xiàn):
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __testBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0)};
struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;?
Desc = desc;
}
};
static int __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself, int count) {
return count ++;
}
函數(shù)具體的實(shí)現(xiàn)如下:
void testBlock(){
int (*blk)(int) = ((int (*)(int))&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 5);
}
雖然編譯后Block方法很長,但是并不復(fù)雜,Block調(diào)用就像普通的C函數(shù)的使用。
- 首先,看一下源代碼中
Block方法體實(shí)現(xiàn):
int (^blk)(int) = ^(int count){
return count ++;
};
static int __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself, int count) {
return count ++;
}
可以發(fā)現(xiàn)兩者在語法格式上非常相近,實(shí)際上Block所使用的匿名函數(shù)就是被作為簡單的C函數(shù)處理的。
在代碼中:
__testBlock_block_impl_0 *__cself
其中,參數(shù)__cself相當(dāng)于C++中指向?qū)嵗陨碜兞康?code>this或者Objective-C中的self。參數(shù)__cself在這里是__testBlock_block_impl_0結(jié)構(gòu)體的指針。
- 然后,看一下
__testBlock_block_impl_0結(jié)構(gòu)體的組成:
struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;
};
去掉代碼中加入的構(gòu)造函數(shù),感覺結(jié)構(gòu)上清晰多了。第一個(gè)成員變量是impl,__block_impl結(jié)構(gòu)體的聲明如下所示:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
其中,有關(guān)對象isa指針的理解可以參考之前寫過的文章Objective-C Runtime:深入理解類與對象
第二個(gè)成員變量Desc,主要保存block所在內(nèi)存的區(qū)域以及Block的大小。
- 關(guān)于
__testBlock_block_impl_0結(jié)構(gòu)體構(gòu)造函數(shù)如下:
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;?
Desc = desc;
}
isa指針指向_NSConcreteStackBlock, __testBlock_block_impl_0結(jié)構(gòu)體相當(dāng)于objc_object結(jié)構(gòu)體的Objective-C類對象的結(jié)構(gòu)體。
isa = &_NSConcreteStackBlock;
上述代碼表明_NSConcreteStackBlock相當(dāng)于objc_class結(jié)構(gòu)體的實(shí)例。從而得知,Block就是Objective-C的對象了。
具體關(guān)于Objective-C類與對象的知識可以參考另一篇文章深入理解類與對象。
對于上述構(gòu)造函數(shù)調(diào)用部分如下:
int (*blk)(int) = ((int (*)(int))&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 5);
看著有些繁瑣,對其去掉相關(guān)轉(zhuǎn)換的部分:
struct __testBlock_block_impl_0 temp = __testBlock_block_impl_0(__testBlock_block_func_0, &__testBlock_block_desc_0_DATA);
struct __testBlock_block_impl_0 *blk = & temp;
(*blk->impl.FuncPtr)(blk, 5);
上述代碼中,將__testBlock_block_impl_0結(jié)構(gòu)體實(shí)例的指針賦值給變量blk,從而知道源代碼中的Block表達(dá)式就是__testBlock_block_impl_0結(jié)構(gòu)體類型的變量,同時(shí)也是該結(jié)構(gòu)體在棧上生成的實(shí)例。
從函數(shù)指針調(diào)用得知,由Block語法轉(zhuǎn)換的__testBlock_block_func_0函數(shù)指針被賦值到__testBlock_block_impl_0的成員變量FuncPtr,同時(shí)說明了__testBlock_block_func_0中的__cself指向Block值。
截獲局部變量
在開發(fā)過程中,我們經(jīng)常遇到在Block中截獲局部變量或者改變局部變量,如下所示:
- (void)testBlock{
int value = 1;
void (^block) (void) = ^{
NSLog(@"value = %d", value);
};
block();
}
看到這里,可能就有疑問了,Block內(nèi)部是如何獲取局部變量的,帶著疑問重新Clang了一下源文件,似乎找到了答案,摘取了關(guān)鍵代碼如下所示:
static void _I_TestBlock_testBlock(TestBlock * self, SEL _cmd) {
int value = 1;
void (*block) (void) = ((void (*)())&__TestBlock__testBlock_block_impl_0((void *)__TestBlock__testBlock_block_func_0, &__TestBlock__testBlock_block_desc_0_DATA, value));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
//Block內(nèi)部實(shí)現(xiàn):
static void __TestBlock__testBlock_block_func_0(struct __TestBlock__testBlock_block_impl_0 *__cself) {
int value = __cself->value; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_pf_nyb_8kl12yq6v2sxzwdr5v8w0000gn_T_TestBlock_516a43_mi_0, value);
}
// Block結(jié)構(gòu)體
struct __TestBlock__testBlock_block_impl_0 {
struct __block_impl impl;
struct __TestBlock__testBlock_block_desc_0* Desc;
// 將局部變量追加成成員變量
int value;
__TestBlock__testBlock_block_impl_0(void *fp, struct __TestBlock__testBlock_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
通過上述Block語法表達(dá)式中使用的局部變量作為成員變量被加到了__TestBlock__testBlock_block_impl_0結(jié)構(gòu)體中,同時(shí),加到結(jié)構(gòu)體中的成員變量的類型與局部變量的類型保持一致。當(dāng)然了,如果Block表達(dá)式中沒有使用局部變量,則不會(huì)加入到Block的結(jié)構(gòu)體中。
通過初始化Block實(shí)例的構(gòu)造方法和調(diào)用也可以看到局部變量value作為參數(shù)傳遞,具體如下所示:
//Block 初始化方法
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int _value, int flags=0) : value(_value)
// 初始化方法的調(diào)用:
void (*block) (void) = ((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA, value));
到這里,大致明白了Block如何截獲局部變量value的,小結(jié)如下:
- 1、系統(tǒng)編譯時(shí),根據(jù)Block是否需要截獲外部變量來判斷是都將局部變量作為成員變量保存到Block的結(jié)構(gòu)體
__testBlock_block_impl_0中; - 2、若需要截獲局部變量,需要在初始化結(jié)構(gòu)體函數(shù)中添加與局部變量相同類型的參數(shù);
- 3、局部變量就通過初始化構(gòu)造函數(shù)傳遞到Block結(jié)構(gòu)體
__testBlock_block_impl_0,同時(shí)賦值給Block中的成員變量; - 4、在執(zhí)行Block時(shí),局部變量
value會(huì)初始化__testBlock_block_impl_0結(jié)構(gòu)體實(shí)例, 如下所示;
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __testBlock_block_func_0;
Desc = &__testBlock_block_desc_0_DATA;
value = 1;
總的來說,截獲局部變量就是將要使用的局部變量保存到Block結(jié)構(gòu)體實(shí)例中。
Block中使用C語言數(shù)組
- 只使用C語言的字符串?dāng)?shù)組,也沒有向捕獲的局部變量賦值,然而下面的代碼再編譯時(shí)就會(huì)產(chǎn)生錯(cuò)誤,如下所示:
int testBlock1(){
const char country[] = "China";
void (^block) (void) = ^{
printf("text=%c", country[1]);
};
block();
return 0;
}
編譯的時(shí)候會(huì)出現(xiàn)錯(cuò)誤:

看到這,你會(huì)感到疑惑,上面剛講到Block是如何截獲局部變量的,為什么到這里就失效了呢?
這是因?yàn)椋?strong>Block中,沒有實(shí)現(xiàn)對C語言數(shù)組的截獲。通過上述截獲局部變量的理論,C語言數(shù)組會(huì)作為成員變量保存到Block實(shí)例的結(jié)構(gòu)體中,在初始化Block時(shí),由成員變量賦值給局部變量,猜測代碼如下:
void func(char a[]){
char b[] = a;
}
從而看出端倪,將C語言數(shù)組類型變量賦值給C語言數(shù)據(jù)類型變量,這個(gè)在C語言下是無法編譯,C語言不允許這樣編譯的。
如何解決呢?
目前,可以改成C語言數(shù)組的形式就可以解決上述問題了。
int testBlock1(){
const char *country = "China";
void (^block) (void) = ^{
printf("text=%c", country[1]);
};
block();
return 0;
}
__block關(guān)鍵字的作用
前面的小節(jié)中,我們僅僅講解了在Block中截獲局部變量,那么,在Block中修改截獲的局部變量又會(huì)帶來什么樣的問題呢?

通過上面的代碼可以看出,雖然在Block中不能對局部變量進(jìn)行修改,但是在全局變量、靜態(tài)全局變量、靜態(tài)變量都是可以在Block中進(jìn)行修改的,具體如下:
int globalValue = 10;
static int staticGlobalValue = 5;
- (void)testBlockFunc {
static int value = 5;
void (^block)(void) = ^{
globalValue = 11;
staticGlobalValue = 12;
value = 3;
printf("globalValue = %d \n staticGlobalValue = %d \n value = %d \n", globalValue, staticGlobalValue, value);
};
block();
}
打印的結(jié)果如下:
2018-12-02 21:56:47.323727+0800 TestBlock[38880:1182883]
globalValue = 11
staticGlobalValue = 12
value = 3
再次使用clang編譯了一下,發(fā)現(xiàn)靜態(tài)全局變量和全局變量轉(zhuǎn)換前后沒有任何變換,大家可能疑問靜態(tài)局部變量是如何轉(zhuǎn)換的呢?
__TestBlockMemory__testBlockFunc_block_impl_0 *__cself) {
int *value = __cself->value; // bound by copy
globalValue = 11;
staticGlobalValue = 12;
(*value) = 3;
printf("globalValue = %d \n staticGlobalValue = %d \n value = %d \n", globalValue, staticGlobalValue, (*value));
}
與截獲局部變量的轉(zhuǎn)換很相似,唯一的區(qū)別是Int類型的變量轉(zhuǎn)換成了指針,從而正確的改變了原有的值。
除了上述的幾種方式可以在Block中修改變量值外,OC專門提供了一個(gè)說明符——__block。
int testBlock(){
__block int number = 1;
void (^block) (void) = ^{
number = 2;
};
block();
return 0;
}
將上述代碼進(jìn)行Clang編譯,還原成以下代碼:
struct __Block_byref_number_0 {
void *__isa;
__Block_byref_number_0 *__forwarding;
int __flags;
int __size;
int number;
};
struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;
__Block_byref_number_0 *number; // by ref
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
__Block_byref_number_0 *number = __cself->number; // bound by ref
(number->__forwarding->number) = 2;
}
驚奇的發(fā)現(xiàn),局部變量number變成了結(jié)構(gòu)體__Block_byref_number_0,Block的__testBlock_block_impl_0的結(jié)構(gòu)體實(shí)例持有__block變量的__Block_byref_number_0結(jié)構(gòu)體實(shí)例的指針。__testBlock_block_impl_0實(shí)例的成員變量__forwarding指向?qū)嵗陨恚⑼ㄟ^__forwarding變量訪問成員變量** number**。
__Block_byref_number_0 *number = __cself->number; // bound by ref
(number->__forwarding->number) = 2;
值得注意一點(diǎn)是,__Block_byref_number_0結(jié)構(gòu)體并沒有在__testBlock_block_impl_0結(jié)構(gòu)體中,這樣主要是為了在多個(gè)Block中共用該__block變量。
Block的儲存
Block分為三類:** _NSConcreteStackBlock**
| 種類 | 存儲域 | Block變量類型 | 復(fù)制后的存儲域 |
|---|---|---|---|
| _NSConcreteStackBlock | 棧 | 局部/自動(dòng)變量 | 由棧復(fù)制到堆上 |
| _NSConcreteGlobalBlock | 程序的數(shù)據(jù)區(qū)域(.data區(qū)) | 全局變量 | 保持在數(shù)據(jù)區(qū) |
| _NSConcreteMallocBlock | 堆 | 局部變量 | 增加引用計(jì)數(shù) |
定義在全局的Block在變量作用域之外通過指針訪問使用,定義在棧上的__block局部變量和Block在超出作用域時(shí)將被廢棄。那Block超出作用域是如何存在的呢?
這里就用到Block的Copy操作,即將分配在棧上的Block 和__block變量復(fù)制到堆上,從而延長了Block和__block變量的生命周期。
那么,Block被復(fù)制到堆上的操作是何時(shí)進(jìn)行的呢?
首先,比較明確的一點(diǎn)是:將Block作為返回值返回時(shí),編譯器將會(huì)自動(dòng)將Block由棧上復(fù)制到堆上,其他情況下,需要手動(dòng)執(zhí)行Copy操作;在ARC有效的情況下,Block是否被復(fù)制到堆上,編譯器會(huì)進(jìn)行相關(guān)的判斷;
舉個(gè)栗子:
typedef int (^BlockCase)(int);
BlockCase getBlockCallBack(int value) {
return ^(int count) {
return count + value;
};
}
在ARC有效的情況下,作為返回值的Block會(huì)被復(fù)制到堆上。當(dāng)然了,系統(tǒng)中存在的一些API是無需手動(dòng)復(fù)制的:Cocoa框架方法中帶有usingBlock方法、GCD相關(guān)的API等等。
__block變量的存儲域
當(dāng)在Block中使用__block變量時(shí),當(dāng)在Block從棧復(fù)制到堆上同時(shí),被使用過的__block變量也會(huì)從棧復(fù)制到堆上。如下圖所示:
| __block變量所分配的存儲域 | Block從棧復(fù)制到堆上后的影響 |
|---|---|
| 棧 | 由棧復(fù)制到堆上并被Block持有 |
| 堆 | 被Block持有 |
__fawarding指針存在的意義
前邊介紹了Block和__block變量從棧復(fù)制到堆上的情景,但都與__fawarding指針脫不開關(guān)系。
- 由棧復(fù)制到堆之前,__fawarding指針指向自身;
- 復(fù)制之后,棧上的__fawarding指針指向復(fù)制到堆上的__block變量,堆上的__fawarding指針指向自身。
從而,解釋了無論__block變量配置在棧上還是配置在堆上時(shí)都能正確的訪問到__block變量;
Block截獲對象
通過了解上節(jié)的內(nèi)容,很好理解以下內(nèi)容:
- __block修飾的變量從棧復(fù)制到堆上,賦值給該__block變量的對象也被從棧復(fù)制到堆上,當(dāng)__block變量從堆上釋放時(shí),該對象才能得到釋放;
- 當(dāng)使用__weak修飾的__block變量在賦值的時(shí)候,由于賦值對象的作用域問題而釋放,從而導(dǎo)致__block變量不能強(qiáng)持有該對象。
Block循環(huán)引用
形成原因:
- 對象與Block相互持有;
- 形成強(qiáng)持有環(huán);
解決方式: - 設(shè)置weak弱持有關(guān)系;
- 手動(dòng)置為nil,梳理好持有關(guān)系,破壞環(huán)狀結(jié)構(gòu)。
結(jié)束
關(guān)于Block就大致說到這吧,從Block的使用到Block的原理剖析,希望能讓我們更客觀的認(rèn)識Block,寫出更高質(zhì)量的代碼。
好久沒有更新文章了,想必老鐵們也都等著急了吧。主要是從2018年3月底更新后最后一篇文章,就一直在忙,關(guān)于那段時(shí)間的事情會(huì)在另一篇文章中詳述。
謝謝大家的支持!
掃一掃下面的二維碼,歡迎關(guān)注我的個(gè)人微信公眾號:猿視角(ID:iOSDevSkills),可在微信公眾號進(jìn)行留言,更多精彩技術(shù)文章,期待您的加入!一起討論,一起成長!一起攻城獅!
