Block是什么?
在oc中它是帶有^符號的匿名函數(shù),遵循BN范式: ^ 返回值類型 參數(shù)列表 表達式 (參數(shù)和返回值為空的時候可以省略)。
而在C中(OC編譯后的C)它會被編譯成一堆結(jié)構(gòu)體和幾個函數(shù)以及靜態(tài)變量。
而這幾個結(jié)構(gòu)體和函數(shù),正是Block實現(xiàn)的本質(zhì).
- Block 結(jié)構(gòu)體
- 表達式轉(zhuǎn)換成的函數(shù)
- 描述體
- Block主體結(jié)構(gòu)體
- Block的調(diào)用
// Block 結(jié)構(gòu)體
struct __block_imp1 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
}
// 描述體
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_imp1_0)
}
// Block主體結(jié)構(gòu)體
struct __main_block_imp1_0 {
struct __block_imp1 imp1;
struct __main_block_desc_0* Desc;
__main_block_imp1_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
imp1.isa = &_NSConcreteStackBlock;
imp1.Flags = flags;
imp1.FuncPtr = fp;
Desc = desc;
}
}
// 表達式轉(zhuǎn)換成的函數(shù)
static void __main_block_func_0(struct __main_block_imp1_0 *__cself) {
printf("this is a block");
}
int main() {
// Block的調(diào)用
void (*blk)(void) = (void(*)(void))&__main_block_imp1_0((void*)__main_block_func_0,&__main_block_desc_0_DATA);
((void(*)(struct __block_imp1 *))((struct __block_imp1 *)blk)->FuncPtr)((struct __block_imp1 *)blk);
return 0;
}
逐一來分析一下,這幾個結(jié)構(gòu)體的作用。
// Block 結(jié)構(gòu)體
struct __block_imp1 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
}
由于轉(zhuǎn)化后的Block類型結(jié)構(gòu)體的成員中有isa指針,不難得知它和某些OC的類有關系,而在struct __main_block_imp1_0的初始化函數(shù)中,isa被賦予了一個_NSConcreteStackBlock的類。這表明該Block屬于stack存在于棧中的類。
_NSConcreteStackBlock 存在于棧中
_NSConcreteMallocBlock 存在于堆中
_NSConcreteGlobalBlock 存在于.Data(程序的數(shù)據(jù)區(qū)域)中
?
// 描述體
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_imp1_0)
}
只是存儲了一些升級區(qū)域和結(jié)構(gòu)大小。
// Block主體結(jié)構(gòu)體
struct __main_block_imp1_0 {
struct __block_imp1 imp1;
struct __main_block_desc_0* Desc;
__main_block_imp1_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
imp1.isa = &_NSConcreteStackBlock;
imp1.Flags = flags;
imp1.FuncPtr = fp;
Desc = desc;
}
}
__main_block_imp1_0結(jié)構(gòu)體從某種程度上封裝了前兩個結(jié)構(gòu)體,但它的實際意義不止于此。
// 表達式轉(zhuǎn)換成的函數(shù)
static void __main_block_func_0(struct __main_block_imp1_0 *__cself) {
printf("this is a block");
}
int main() {
// Block的調(diào)用
void (*blk)(void) = (void(*)(void))&__main_block_imp1_0((void*)__main_block_func_0,&__main_block_desc_0_DATA);
((void(*)(struct __block_imp1 *))((struct __block_imp1 *)blk)->FuncPtr)((struct __block_imp1 *)blk);
return 0;
}
函數(shù)和調(diào)用很簡單,就是一些類型轉(zhuǎn)換容易讓人眼花繚亂,經(jīng)過刪繁就簡之后,就是簡單的函數(shù)聲明和調(diào)用而已。
需要注意的是,__main_block_func_0是以__main_block_imp1_0這個主體結(jié)構(gòu)體入?yún)⒌模@在變量捕捉的時候有很大作用。
變量捕捉
熟悉了Block的本質(zhì)之后,就是對于Block的運用了,Block為什么如此好用的原因之一就在于,我們可以做到一般函數(shù)不能做到的事情,例如:局部變量的捕捉。
int main() {
int v = 1;
void (^blk)(void) = ^{
printf("this is a block %d",v);
};
blk();
return 0;
}
可以看到,變量v能夠在表達式內(nèi)部獲取到。
然而當我們想要在內(nèi)部改變這個變量的值的時候,會得到一個編譯錯誤。
int main() {
int v = 1;
void (^blk)(void) = ^{
printf("this is a block %d",v);
v = 2; // error: Variable is not assignable (missing __block type specifier)
};
blk();
return 0;
}
為什么?
既然錯誤發(fā)生在表達式內(nèi),我們就來分析一下編譯后的函數(shù)static void __main_block_func_0(struct __main_block_imp1_0 *__cself) 捕獲變量后,這個函數(shù)的實現(xiàn)變成了這樣:
// 表達式轉(zhuǎn)換成的函數(shù)
static void __main_block_func_0(struct __main_block_imp1_0 *__cself) {
int v = __cself->v;
printf("this is a block %d",v);
}
入?yún)?code>__main_block_imp1_0多了個v的成員變量。
也就是說,編譯后,__main_block_imp1_0結(jié)構(gòu)體負責存儲捕獲的局部變量。
然而捕獲的變量也是Int類型的,并非指針,所以我們在__main_block_func_0中改變變量V的值是沒有任何意義的。編譯器很聰明的檢測到了這個操作。
那么我們加入__block之后呢?
int main() {
__block int v = 1;
void (^blk)(void) = ^{
printf("this is a block %d",v);
v = 2;
};
blk();
return 0;
}
編譯器的報錯消失了。
意味著我們可以隨意在block內(nèi)改動v這個局部變量了。
那么內(nèi)部是怎么實現(xiàn)的呢?
還是看函數(shù)實現(xiàn):
// 表達式轉(zhuǎn)換成的函數(shù)
static void __main_block_func_0(struct __main_block_imp1_0 *__cself) {
__Block_byref_val_0 *val = __cself->val;
printf("this is a block %d",(val->_forwarding->v));
(val->_forwarding->v) = 2;
}
__main_block_imp1_0里存儲的成員V變了,不再是簡單的Int類型,而是變成了
__Block_byref_val_0 指針。
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int __v;
}
需要稍微分析一下,這幾個成員變量的意義。
找到它初始化的地方,在main函數(shù)內(nèi):
__Block_byref_val_0 v = (
0,
&v,
0,
sizeof(__Block_byref_val_0),
1
)
isa 被賦值為空
_forwarding 指向了自己,
flags =0
size 傳入了大小
v 賦值成捕獲的變量的值
而原本的int變量v消失了,__Block_byref_val_0變量v取而代之,將此結(jié)構(gòu)體的指針賦值給
__main_block_imp1_0成員變量后,就可以實現(xiàn)對變量v的修改了。
這是__block的作用,可是在結(jié)構(gòu)體
__Block_byref_val_0初始化的時候還有一個有趣的地方,就是:
__Block_byref_val_0 *__forwarding;
__forwarding指針有什么意義呢?它指向了自己,并且在函數(shù)體內(nèi)修改存儲值時,也訪問了"自己"。為何要多此一舉?
Block的存儲域
再弄清__forwarding指針的作用之前,我們先要縷清楚這三種Block的區(qū)別以及應用的場景。
| 類型 | 內(nèi)存分布 | 場景 |
|---|---|---|
| _NSConcreteStackBlock | 存在于棧中 | 定義在函數(shù)內(nèi)部,未被強引用,且捕獲了外部變量。 |
| _NSConcreteMallocBlock | 存在于堆中 | 被強引用,由_NSConcreteStackBlock被拷貝到堆內(nèi)存。 |
| _NSConcreteGlobalBlock | 存在于.Data(程序的數(shù)據(jù)區(qū)域)中 | 定義在全局區(qū)域,或者未捕獲外部變量且定義在函數(shù)內(nèi)部的block |
從內(nèi)存管理的角度看,_NSConcreteGlobalBlock類型的Block討論意義不大。
_NSConcreteStackBlock和 _NSConcreteMallocBlock 才是我們要討論的重點。
假定這樣一種情況(MRC下):
blk_t blk;
{
NSAutoreleasePool *pool = [NSAutoreleasePool new];
id array = [[[NSMutableArray alloc] init] autorelease];
blk = ^( id obj){
[array addObject:obj];
NSLog(@"count is %d",[array count]);
};
[pool release];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
由于是MRC環(huán)境,加入NSAutoreleasePool來模擬array的釋放周期。
可以看到,在大括號以外調(diào)用block,肯定會崩潰,因為array已經(jīng)釋放了。并且在這種情況下,Block屬于_NSConcreteStackBlock類型。
我們稍稍改動一下:
blk_t blk;
{
NSAutoreleasePool *pool = [NSAutoreleasePool new];
id array = [[[NSMutableArray alloc] init] autorelease];
blk = [^( id obj){
[array addObject:obj];
NSLog(@"count is %d",[array count]);
} copy];
[pool release];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
程序輸出正常了,array也被block正常捕獲。
而Block的類型經(jīng)過copy之后,變成了_NSConcreteMallocBlock,copy方法實際調(diào)用的是:
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
_Block_copy 作用按照文檔上來說是將Block拷貝到堆內(nèi)存——這和我們的測試結(jié)果一樣。
隨著Block被復制到堆內(nèi)存之后,Block所持有的__block變量也會復制到堆內(nèi)存并且持有,要證明需要看編譯后的代碼:
static void __main_block_copy_0 (struct __main_block_imp1_0*dst ,struct __main_block_imp1_0*src){
_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
static void __main_block_dispose_0(struct __main_block_imp1_0*scr){
_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}
這兩個方法是在Block被復制到堆內(nèi)存和從堆內(nèi)存釋放的時候調(diào)用的。
_Block_object_assign 和 _Block_object_dispose分別是Block持有變量的賦值和釋放。
__forwarding指針:
這時候我們可以來解釋這個指針的作用了:
__block int v = 1;
void (^blk)(void) = [^{v++;} copy];
v++;
blk();
printf("num is %d",v);
可以看到經(jīng)過copy后的變量v在外部仍然可以改變,試想如果沒有__forwarding指針存在的話,v經(jīng)過編譯后,結(jié)構(gòu)體仍然存在于棧上,此時改變v的值結(jié)果肯定不是我們想要的3。
__forwarding保證__Block的變量在賦值時使得位于棧上的結(jié)構(gòu)體內(nèi)的__forwarding指針指向堆內(nèi)的Block,從內(nèi)訪問同一個且正確的變量。