一句話總結(jié)block : 帶有局部變量的匿名函數(shù)
閉包在其它編程語言的名稱
編程語言 | Block名稱 |
---|---|
C+Block | Block |
Smalltalk | Block |
Ruby | Block |
LISP | Lambda |
Python | Lambda |
C++ 11 | Lambda |
Javascript | Anonymous function |
iOS閉包的聲明與定義
博主iOS開發(fā)哮伟,下面從iOS角度入手講解如何理解block閉包:
在OC中聲明一個block的形式如下:
返回值類型 (^block名) (形參列表)
在OC中定義一個block的形式如下:
^ 返回值類型 (形參列表) {表達式}
舉一個賦值的例子:
int (^blk) (int a , int b) = ^ int (int a , int b) { return a + b;};
這是規(guī)范的寫法绸狐,有時我們也可以使用簡略后的寫法:
^ 返回值類型 (形參列表) {表達式}
(省略) (如空施绎,省略)
上面的例子變成:
int (^blk) (int a , int b) = ^(int a , int b) {return a + b;};
但是搪哪,每次聲明block時都需要書寫這么長的表達式顯得不那么方便亲族,我們像使用函數(shù)指針類型時那樣崔慧,使用typedef來解決該問題:
typedef int (^blk_t) (int a , int b);
這樣在聲明一個block變量時拂蝎,就變?yōu)椋?/p>
blk_t blk;
blk = ^(int a , int b ) {return a + b;}
從C的角度看block閉包
block的實質(zhì)
clang(LLVM編譯器)的"-rewrite-objc"選項可將Objective-C的源代碼重寫成C++的源代碼,我們可以將帶有block的OC代碼轉(zhuǎn)成C++代碼惶室,看一看蘋果的實現(xiàn)温自。
clang -rewrite-objc 源代碼文件名
寫一段帶有block的代碼:
typedef int (^blk_t) (int a , int b);
int main(int argc, const char * argv[]) {
@autoreleasepool {
blk_t blk = ^ int (int a , int b) {
return a + b;
};
int c = blk(1,2);
NSLog(@"%d",c);
}
return 0;
}
轉(zhuǎn)換成C++代碼 (由于生成大量的輔助代碼,這里只展示關(guān)鍵代碼)
typedef int (*blk_t) (int a , int b);
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;
}
};
static int __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
return a + b;
}
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;
blk_t blk = ((int (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
int c = ((int (*)(__block_impl *, int, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 1, 2);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_4c_fgfj11vj2t985qr4_0px2sbw0000gn_T_main_a3fdb5_mi_4,c);
}
return 0;
}
在C++源碼中皇钞,block最終被轉(zhuǎn)化成函數(shù)來處理悼泌,blk_t閉包被轉(zhuǎn)換成了結(jié)構(gòu)體,blk_t類型的blk變量初始化時調(diào)用了__main_block_impl_0結(jié)構(gòu)體的構(gòu)造函數(shù)夹界,blk中存儲的是構(gòu)造后的__main_block_impl_0結(jié)構(gòu)體的首地址馆里。
下面來分解下上面的代碼:
//block轉(zhuǎn)化后的結(jié)構(gòu)體類型
struct __main_block_impl_0 {
//成員結(jié)構(gòu)體變量 保存block需要調(diào)用的函數(shù)地址和一些輔助信息
struct __block_impl impl;
//成員結(jié)構(gòu)體變量 保存block大小和一個保留字段
struct __main_block_desc_0* Desc;
//構(gòu)造函數(shù) 函數(shù)名是由編譯器根據(jù)block所處的函數(shù)名和block在該函數(shù)中出現(xiàn)的順序值 (此處0)所生成的
__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;
}
};
//記錄block需要調(diào)用的函數(shù)地址和一些輔助信息
struct __block_impl {
//指向class_t結(jié)構(gòu)體實例
void *isa;
//一般為0
int Flags;
//保留字段 為后續(xù)升級做準備
int Reserved;
//需要調(diào)用的函數(shù)地址指針
void *FuncPtr;
};
//block大小描述結(jié)構(gòu)體
static struct __main_block_desc_0 {
//保留字段 為后續(xù)升級做準備
size_t reserved;
//描述block大小的結(jié)構(gòu)體
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};//直接進行賦值
//block所需調(diào)用的函數(shù)的定義(static代表函數(shù)的作用域僅限于本文件,不必擔心與其他文件中的函數(shù)同名)
static int __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
return a + b;
}
上面的函數(shù)參數(shù)表中可柿,多了一個struct __main_block_impl_0 *__cself參數(shù)鸠踪,這個參數(shù)類似于C++中的this,類似于OC中的self复斥,是用來指向調(diào)用函數(shù)本身對象的指針营密,因為我們可能會在對象的函數(shù)體內(nèi)進行一些對象的成員變量的獲取或修改或調(diào)用對象的其他成員函數(shù),因為block所調(diào)用的函數(shù)被解釋成了一個C語言形式全局函數(shù)目锭,這個函數(shù)并不知道是哪個對象在調(diào)用它评汰,所以我們需要將調(diào)用它的對象指針傳遞進去纷捞。即使這個函數(shù)沒有int a,int b這兩的參數(shù),參數(shù)表為空被去,struct __main_block_impl_0 *__cself參數(shù)也是必不可少的主儡,必須作為函數(shù)參數(shù)表中的第0個參數(shù)傳入。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
//blk變量的初始化惨缆,調(diào)用__main_block_impl_0的構(gòu)造函數(shù)糜值,并將其轉(zhuǎn)化成了block
blk_t blk = ((int (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//將__main_block_impl_0類型的結(jié)構(gòu)體轉(zhuǎn)換成__block_impl類型結(jié)構(gòu)體,并獲取需要調(diào)用的函數(shù)指針踪央,調(diào)用函數(shù)
int c = ((int (*)(__block_impl *, int, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 1, 2);
//輸出返回值
NSLog((NSString *)&__NSConstantStringImpl__var_folders_4c_fgfj11vj2t985qr4_0px2sbw0000gn_T_main_a3fdb5_mi_4,c);
}
return 0;
}
因為__block_impl結(jié)構(gòu)體是__main_block_impl_0結(jié)構(gòu)體的成員變量臀玄,且是第一個成員變量,那么__main_block_impl_0的指針和__block_impl指針的值是相同的(如果不太明白可以去查下結(jié)構(gòu)體在內(nèi)存上的分配)畅蹂,所以在main函數(shù)中可以將__main_block_impl_0的指針強轉(zhuǎn)成__block_impl的指針健无。
在main中,去掉轉(zhuǎn)化部分液斜,便簡化為:
(*blk->impl.FuncPtr)(blk,1,2);
總結(jié):block類型的變量累贤,會被轉(zhuǎn)換成 : 結(jié)構(gòu)體 + 函數(shù)的形式,在內(nèi)存上的形式時是以結(jié)構(gòu)體變量進行存儲的少漆。結(jié)構(gòu)體當中保存有調(diào)用函數(shù)的指針臼膏,當block發(fā)生調(diào)用時,會從結(jié)構(gòu)體中將函數(shù)指針的值取出示损,根據(jù)函數(shù)指針來調(diào)用函數(shù)渗磅,以此便實現(xiàn)了block的調(diào)用。
block捕獲變量
在程序中检访,共有如下幾種類型的變量:
- 局部變量
- 靜態(tài)變量
- 靜態(tài)全局變量
- 全局變量
block的變量完全符合上述類型變量始鱼。
1) block捕獲局部變量
typedef void (^blk_t1) (void);
int main(int argc, const char * argv[]) {
//捕獲局部變量
int d = 10;
blk_t1 blk1 = ^ {
NSLog(@"%d",d);
};
d = 11;
blk1();
}
打印的結(jié)果為:
10
雖然局部變量d的值實在調(diào)用block之前修改的,但是block打印出的值仍未修改之前的脆贵,這說明block捕獲的是局部變量瞬時的值医清。 下面將源碼轉(zhuǎn)換成C++源碼:
//其他結(jié)構(gòu)同上面block實質(zhì)中代碼相同 這里展示不同部分
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int d;
__main_block_impl_0(void *fp, struct __main_block_desc_1 *desc, int _d, int flags=0) : d(_d) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
int main(int argc, const char * argv[]) {
int d = 10;
blk_t1 blk1 = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, d));
d = 11;
((void (*)(__block_impl *))((__block_impl *)blk1)->FuncPtr)((__block_impl *)blk1);
}
//簡化后
int main(int argc, const char * argv[]) {
int d = 10;
blk_t1 blk1 = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, d));
d = 11;
(*blk1->impl.FuncPtr)(blk1);
}
可以看到__main_block_impl_0
結(jié)構(gòu)體中多出了一個int d成員變量,在main中d修改之前d就已經(jīng)作為參數(shù)傳遞給了__main_block_impl_0
結(jié)構(gòu)體構(gòu)造函數(shù)了卖氨,這時__main_block_impl_0
結(jié)構(gòu)體中的int d
成員的值已經(jīng)賦值完畢会烙,在調(diào)用block時就會打印成員變量d
的值。所以局部變量d
的值的改變不會再影響到block中的d
的值筒捺。
2)修改局部變量
typedef void (^blk_t1) (void);
int main(int argc, const char * argv[]) {
//修改局部變量
int d = 10;
blk_t1 blk1 = ^ {
d = 12;
NSLog(@"%d",d);
};
blk1();
NSLog(@"%d",d);
}
這樣是行不通的柏腻,直接報錯:
Variable is not assignable (missing __block type specifier)
提示我們?nèi)鄙?code>__block修飾符。更改后代碼如下:
typedef void (^blk_t1) (void);
int main(int argc, const char * argv[]) {
//修改局部變量
__block int d1 = 10;
blk_t1 blk1 = ^ {
d1 = 12;
NSLog(@"%d",d1);
};
blk1();
NSLog(@"%d",d1);
}
打印的結(jié)果為:
12
12
達到預(yù)期系吭,下面將源碼轉(zhuǎn)換成C++源碼:
struct __Block_byref_d1_0 {
void *__isa;
__Block_byref_d1_0 *__forwarding;
int __flags;
int __size;
int d1;
};
struct __main_block_impl_2 {
struct __block_impl impl;
struct __main_block_desc_2* Desc;
__Block_byref_d1_0 *d1; // by ref
__main_block_impl_2(void *fp, struct __main_block_desc_2 *desc, __Block_byref_d1_0 *_d1, int flags=0) : d1(_d1->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_2(struct __main_block_impl_2 *__cself) {
__Block_byref_d1_0 *d1 = __cself->d1; // bound by ref
(d1->__forwarding->d1) = 12;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_4c_fgfj11vj2t985qr4_0px2sbw0000gn_T_main_c7e4e3_mi_2,(d1->__forwarding->d1));
}
int main(int argc, const char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_d1_0 d1 = {(void*)0,(__Block_byref_d1_0 *)&d1, 0, sizeof(__Block_byref_d1_0), 10};
blk_t1 blk2 = ((void (*)())&__main_block_impl_2((void *)__main_block_func_2, &__main_block_desc_2_DATA, (__Block_byref_d1_0 *)&d1, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk2)->FuncPtr)((__block_impl *)blk2);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_4c_fgfj11vj2t985qr4_0px2sbw0000gn_T_main_c7e4e3_mi_3,(d1.__forwarding->d1));
};
}
可以看到五嫂,通過__block修飾符修飾的變量,都會以結(jié)構(gòu)體的形式在block結(jié)構(gòu)體重存在村斟,而不是向之前直接以基本類型int d的方式存在贫导。struct __Block_byref_d1_0
結(jié)構(gòu)體中保存了d1的值,和一些其它的輔助信息:
struct __Block_byref_d1_0
結(jié)構(gòu)體中的__Block_byref_d1_0 *__forwarding
成員變量蟆盹,保存的是自身的地址值孩灯,在static void __main_block_func_2
函數(shù)中,(d1->__forwarding->d1) = 12;
使用forwarding來獲取到d1的值逾滥。原因在下文呈現(xiàn)峰档。
3)修改靜態(tài)變量
int main(int argc, const char * argv[]) {
//修改靜態(tài)變量
static int d2 = 10;
blk_t1 blk3 = ^ {
d2 = 13;
NSLog(@"%d",d2);
};
blk3();
NSLog(@"%d",d2);
}
打印的結(jié)果為:
13
13
與局部變量不同的是,在block內(nèi)部修改靜態(tài)變量是不需要添加__block
修飾符的,可以直接修改靜態(tài)變量的值寨昙。原因如下:
struct __main_block_impl_3 {
struct __block_impl impl;
struct __main_block_desc_3* Desc;
int *d2;
__main_block_impl_3(void *fp, struct __main_block_desc_3 *desc, int *_d2, int flags=0) : d2(_d2) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_3(struct __main_block_impl_3 *__cself) {
int *d2 = __cself->d2; // bound by copy
(*d2) = 13;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_4c_fgfj11vj2t985qr4_0px2sbw0000gn_T_main_60608c_mi_4,(*d2));
}
靜態(tài)變量會保存在內(nèi)存的數(shù)據(jù)段讥巡,它的地址是固定分配的,所以block可以直接根據(jù)靜態(tài)變量的地址去操作靜態(tài)變量舔哪,不需要做額外的信息保存操作欢顷。
4)修改靜態(tài)全局變量
static int d3;
int main(int argc, const char * argv[]) {
//修改靜態(tài)全局變量
blk_t1 blk4 = ^ {
d3 = 14;
NSLog(@"%d",d3);
};
blk4();
NSLog(@"%d",d3);
}
打印結(jié)果為:
14
14
由于d3為靜態(tài)全局變量,d3擁有全局作用范圍捉蚤,所以直接在所有函數(shù)中使用d3就可以對d3進行操作抬驴,不必向靜態(tài)變量那樣使用指針來去找到變量。
struct __main_block_impl_4 {
struct __block_impl impl;
struct __main_block_desc_4* Desc;
__main_block_impl_4(void *fp, struct __main_block_desc_4 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_4(struct __main_block_impl_4 *__cself) {
d3 = 14;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_4c_fgfj11vj2t985qr4_0px2sbw0000gn_T_main_2fa84d_mi_6,d3);
}
4)修改全局變量
int d4;
int main(int argc, const char * argv[]) {
//修改全局變量
blk_t1 blk5 = ^ {
d4 = 15;
NSLog(@"%d",d4);
};
blk5();
NSLog(@"%d",d4);
}
打印結(jié)果為:
15
15
由于修改全局變量和修改靜態(tài)全局變量在底層的實現(xiàn)方式是一模一樣的缆巧,這里就不再展示轉(zhuǎn)換后的源碼了布持。
block的生命周期
block可以向C語言變量一樣使用,block在OC中的形式也是以對象的形式存在的陕悬。那么block也是有自己的聲明周期的题暖,如果block的作用域是在一個函數(shù)內(nèi)的,那么block的生命周期就是函數(shù)調(diào)用期間在棧上存在的期間捉超。一旦函數(shù)執(zhí)行完畢胧卤,函數(shù)中的變量從棧幀中彈出,那么block的生命周期隨即結(jié)束狂秦。
有時灌侣,我們需要讓block的生命周期變長一些來滿足業(yè)務(wù)需要,我們會將block從棧上拷貝到堆上裂问,這時就涉及到一個問題侧啼,block中引用的變量怎么辦?靜態(tài)變量和局部變量還好堪簿,引用局部變量就變得比較棘手痊乾。下面就來回答上面使用__block
修飾符修改局部變量所留下來的問題:
block 與 __block變量的實質(zhì):
名稱 | 實質(zhì) |
---|---|
block | 棧上block的結(jié)構(gòu)體實例 |
__block變量 | 棧上__block變量的結(jié)構(gòu)體實例 |
在OC中,block被當做類來處理椭更,block有一下三種類:
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
block不同類在內(nèi)存上的存儲區(qū):
類 | 設(shè)置對象的存儲區(qū)域 |
---|---|
_NSConcreteStackBlock | 棧 |
_NSConcreteGlobalBlock | 數(shù)據(jù)段 |
_NSConcreteMallocBlock | 堆 |
內(nèi)存區(qū)域劃分如下:
分配在數(shù)據(jù)段的全局block哪审,可以在整個程序的作用域中通過指針來使用。棧上的block當函數(shù)調(diào)用完畢即銷毀虑瀑。堆上的block當沒有對象在對其進行引用時湿滓,也會被釋放滴须,但是一般的我們會將棧上的block拷貝到堆上,延長block的生命周期叽奥。
在OC中扔水,我們對一個對象調(diào)用copy方法,便會將對象拷貝到堆上去朝氓,并得到一個指向堆上對象的指針魔市。如果block中有__block
類型的變量,那么__block
類型的變量也會一同被復(fù)制到堆上去赵哲。
通過__block
修飾的類型都會以結(jié)構(gòu)體的方式來存儲表示:
struct __Block_byref_d1_0 {
void *__isa;
__Block_byref_d1_0 *__forwarding;
int __flags;
int __size;
int d1;
};
當block拷貝時待德,會將棧中block中的struct __Block_byref_d1_0
結(jié)構(gòu)體中__forwarding指針指向堆上的那個struct __Block_byref_d1_0
結(jié)構(gòu)體,而不是棧上的枫夺。此后将宪,棧上的block和堆上的block都使用堆上的__block
類型變量,以此來達到數(shù)據(jù)共享與一致性橡庞。這是因為涧偷,當棧上的block釋放后,堆上的block依然需要使用__block
類型的變量毙死,所以需要在堆上開辟空間來存放__block
類型變量燎潮,而堆上的__block
變量又必須與棧上的block
對象保持一致,所以就干脆讓棧上的block和堆上的block都對堆上的__block
變量進行操作扼倘。
由block導致的內(nèi)存泄露問題
在一些使用引用計數(shù)來進行內(nèi)存管理的垃圾回收機制确封,block很容易行程循環(huán)引用而導致內(nèi)存泄露。
舉個例子:
int main(int argc, const char * argv[]) {
//產(chǎn)生循環(huán)引用 導致內(nèi)存泄露
CopyBlock *copyBlock = [[CopyBlock alloc] init];
blk_t3 blk6 = ^ (id obj){
NSLog(@"%@",copyBlock);
};
copyBlock.blk = [blk6 copy];
}
blk6 調(diào)用copy后復(fù)制到堆區(qū)再菊,并對copyBlock 進行了引用持有爪喘。而copyBlock 對象調(diào)用alloc在堆上申請空間,并對blk6在堆上的對象進行持有纠拔,現(xiàn)在blk6堆上的對象與copyBlock相互持有秉剑,循環(huán)引用,導致這兩個對象使用占有空間稠诲,不會釋放侦鹏,垃圾回收機制無法回收,導致內(nèi)存泄露臀叙。
可能有人會問略水,為什么要讓blk6調(diào)用copy方法,因為如果blk6不調(diào)用copy方法劝萤,那么blk6是就在棧上的一個變量渊涝,而調(diào)用后,會在堆上對棧上的blk6進行一個拷貝,堆棧上現(xiàn)在同時有blk6跨释,以為棧上的blk6當函數(shù)調(diào)用完畢會自動回收胸私,我們控制不了,所以在堆上對blk6進行一個拷貝鳖谈,來使程序?qū)е聝?nèi)存泄露盖文。而我們大多數(shù)時候都是面對對象編程,會反復(fù)的在堆上申請空間蚯姆,block變量也會反復(fù)在堆上申請空間進行保存,所以才這么做來演示內(nèi)存泄露洒敏。
如果我的文章對你有用龄恋,不妨在github上給我一個star吧!
代碼地址