block是C語言的一項重要的特性钦购。在很多其他計算機語言中都有類似的概念,比如lamda表達式褂萧,閉包等押桃。那么block是什么?簡而言之导犹,block是帶有自動變量的匿名函數(shù)唱凯。本文將以這句話展開,對block進行深入解析谎痢。
block語法
block可以認為是匿名的C函數(shù)磕昼,它的語法格式是這樣的:
^ int (int i){
return 0;
}
block變量
當(dāng)不需要返回值或函數(shù)參數(shù)時,也可以將這兩部分都省略掉节猿。我們可以將block代碼塊賦值給block類型的變量票从,用法是這樣的:
void (^blk)(int) = ^ void (int i) {
printf("I am block.");
}
blk即為block類型的變量漫雕,它與普通的C語言變量是一樣的,可以用作自動變量峰鄙、函數(shù)參數(shù)浸间、靜態(tài)局部變量、靜態(tài)全局變量吟榴,全局變量魁蒜。可以看到吩翻,block類型的變量定義的方式跟其他變量很是不同兜看,直觀上不易閱讀,所以我們可以使用typedef來給block類型起一個別名仿野,例如這樣:
typedef void (^block_t)(int);
//使用block_t類型定義block變量
block_t blk;
這樣block類型的變量定義就和普通的C語言變量定義看起來一致了铣减,代碼也變得更加直觀。
block截獲的自動變量
理解了block是匿名函數(shù)脚作,再來看看帶有自動變量是怎么回事葫哗。事實上,這里指的是在block內(nèi)部可以截獲外部的自動變量值球涛,示例如下:
typedef void (^block_t)();
int main ()
{
int a = 0;
int b = 1;
block_t blk = ^ () {
printf("block: %d", a);
}
a = 2;
blk();
return 0;
}
在這里劣针,block截獲了自動變量a的值,即便之后a值改變亿扁,并不會影響block中截獲的值捺典,換句話說,上述代碼輸出的結(jié)果是0从祝,而不是2襟己。當(dāng)我們想在block中修改自動變量的值時,會報編譯錯誤牍陌,因為默認情況下擎浴,block內(nèi)是不能修改自動變量的值的。如果我們想要修改毒涧,需要在自動變量前加__block修飾符贮预。為什么加了此修飾符就可以實現(xiàn)修改自動變量呢,這個我們后面詳述契讲。我們先來看個例子來深入理解“不能修改自動變量值”這句話仿吞。
typedef void (^block_t)();
int main ()
{
NSMutableArray *array = [[NSMutableArray alloc] init];
block_t blk = ^ () {
id object = [[NSObject alloc] init];
[array addObject:object]; //正確
array = otherArray; //錯誤
}
blk();
return 0;
}
雖然block內(nèi)不允許修改自動變量的值,這里截獲的是類的對象捡偏,用C語言來描述的話唤冈,這里截獲的是類對象對應(yīng)的結(jié)構(gòu)體實例的指針,我們不能修改指針银伟,但使用指針是沒有問題的你虹。需要指出的是凉当,block并未實現(xiàn)截獲C語言字符數(shù)組,所以我們無法在block內(nèi)使用外部定義的C語言字符數(shù)組售葡,不過我們可以用字符指針char*類型代替。
block實現(xiàn)原理
接下來我們通過分析源碼這種激動人心的方式來理解block的實現(xiàn)原理忠藤。我們可以利用clang這個工具將block代碼轉(zhuǎn)換成C++源碼來分析挟伙。我們在main.m文件中實現(xiàn)一段簡單的block代碼:
int main () {
void (^blk)() = ^() {
int a = 0;
};
blk();
}
接下來,我們使用clang -rewrite-objc main.m命令轉(zhuǎn)換為C++源碼模孩,由于源碼太長尖阔,我們將關(guān)鍵部分摘出來:
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 void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = 0;
}
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 () {
void (*blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
通過對比,很容易發(fā)現(xiàn)我們的block代碼塊實際上轉(zhuǎn)換為了普通C語言函數(shù)__main_block_func_0榨咐,函數(shù)命名上表示該block是main函數(shù)中第一個block介却。函數(shù)的參數(shù)是一個指向__main_block_impl_0結(jié)構(gòu)體的指針,事實上這個結(jié)構(gòu)體就是block對應(yīng)的結(jié)構(gòu)體實例块茁。這個結(jié)構(gòu)體中只有兩個成員變量齿坷,其類型分別是__block_impl和__main_block_desc_0,__block_impl結(jié)構(gòu)體中記錄了block的一些信息数焊,例如函數(shù)指針等永淌,__main_block_desc_0結(jié)構(gòu)體中表示了block的size。另外還有一個構(gòu)造函數(shù)佩耳,對__main_block_impl_0的成員變量做初始化遂蛀。
最后我們來看下main函數(shù)。我們?nèi)サ衾锩娴念愋娃D(zhuǎn)換部分干厚,簡化如下:
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;
(*blk->impl.FuncPtr)(blk);
對比我們的block代碼李滴,前兩行表示了將block實現(xiàn)賦值給block類型的變量,從第三行可以看出蛮瞄,blk的調(diào)用變成了函數(shù)指針的調(diào)用所坯,同時函數(shù)指針中傳入了block變量作為參數(shù)。現(xiàn)在我們已經(jīng)明白裕坊,block其實是用C語言函數(shù)和結(jié)構(gòu)體來實現(xiàn)的包竹。
block截獲自動變量的原理
那么block是怎么截獲自動變量的呢?我們修改下block代碼籍凝,再用clang看下源碼實現(xiàn):
int main () {
int a = 0;
void (^blk)() = ^() {
int b = a + 1;
};
blk();
}
轉(zhuǎn)換后我們來看下源碼相比之前有何變化:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int b = a + 1;
}
可以發(fā)現(xiàn)周瞎,在block對應(yīng)的結(jié)構(gòu)體__main_block_impl_0中多了一個int型成員a,這個成員正是保存了截獲的自動變量a的值饵蒂,然后在__main_block_func_0函數(shù)中声诸,通過指向block自身的指針訪問成員a來獲取自動變量a的值。這下便清楚了退盯,block是將自動變量的值保存在block結(jié)構(gòu)體實例中彼乌,使用時再通過指向block的指針訪問泻肯,這也就解釋了為什么block內(nèi)無法修改外部自動變量值的問題。
__block修改自動變量值的原理
我們再來通過源碼分析:
int main () {
int __block a = 0;
void (^blk)() = ^() {
a = 2;
};
blk();
}
轉(zhuǎn)換后發(fā)現(xiàn)源碼驟然增多慰照,我們摘出關(guān)鍵代碼來分析:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 2;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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 () {
__Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 0};
void (*blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
我們來看引入__block后源碼發(fā)生的變化灶挟。我們依舊從__main_block_func_0這個函數(shù)入手,可以發(fā)現(xiàn)原本的自動變量竟然轉(zhuǎn)化成了一個結(jié)構(gòu)體毒租!再去看main函數(shù)稚铣,第一行就是使用自動變量a初始化了一個結(jié)構(gòu)體__Block_byref_a_0,在這個結(jié)構(gòu)體中墅垮,有一個成員變量a惕医,它保存了外部自動變量a的值,__forwarding是指向結(jié)構(gòu)體自身的指針算色。而在__main_block_impl_0結(jié)構(gòu)體中則是有一個指向__Block_byref_a_0結(jié)構(gòu)體的指針抬伺。這里就清楚了,__block變量轉(zhuǎn)換成結(jié)構(gòu)體時其地址也通過指針的方式被保存了下來灾梦,通過指針當(dāng)然可以實現(xiàn)修改自動變量值的目的(當(dāng)然前提是該自動變量沒有被廢棄峡钓,因為一旦作用域結(jié)束,棧上的變量就會被廢棄斥废,那么指針就成為野指針了椒楣。但事實上,block往往能跨其作用域而存在牡肉,并修改自動變量的值捧灰。這是為什么?我們后面詳細來說)统锤。
block修改靜態(tài)變量值的原理
值得一提的是毛俏,在block內(nèi),除了可以修改__block變量的值外饲窿,還可以修改全局變量煌寇,靜態(tài)全局變量,靜態(tài)局部變量的值逾雄。我們重點看一下靜態(tài)局部變量在源碼中是怎么處理的:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *a = __cself->a; // bound by copy
(*a) = 2;
}
可以看出阀溶,源碼里通過指向靜態(tài)局部變量的指針進行變量訪問和賦值。那為什么自動變量不采用這種方式來改值呢鸦泳?道理很簡單银锻,靜態(tài)局部變量存在于整個應(yīng)用程序生命周期中,不用擔(dān)心會出現(xiàn)野指針的問題做鹰,而自動變量在函數(shù)作用域結(jié)束時就被廢棄了击纬,指針也就無法訪問了。
現(xiàn)在還剩下的問題
到目前為止钾麸,我們還有兩個問題沒有詳細提及:
- block為何可以超出其作用域而存在更振?
- 加入__block修飾符后源碼中新增的copy與dispose方法是什么作用炕桨?
block超出作用域存在的原因
觀察block結(jié)構(gòu)體實例的實現(xiàn),可以發(fā)現(xiàn)在__block_impl結(jié)構(gòu)體中有一ISA指針肯腕。我們知道每一個OC對象的底層實現(xiàn)是一個包含了isa成員變量的結(jié)構(gòu)體献宫,而block結(jié)構(gòu)體正好符合了這一點。事實上实撒,block也是一個OC的對象遵蚜,其isa指向了block所屬的class,從源碼看奈惑,該class是_NSConcreteStackBlock,顧名思義睡汹,該class表示存在于棧上的block肴甸,事實上,block根據(jù)其存儲域可分為三類:
- _NSConcreteGlobalBlock囚巴,存在于靜態(tài)數(shù)據(jù)區(qū)的block
- _NSConcreteStackBlock原在,存在于棧上的block
- _NSConcreteMallocBlock,存在于堆上的block
我們前面看到的block都是_NSConcreteStackBlock類型的彤叉,那么什么時候block會在數(shù)據(jù)區(qū)存儲呢庶柿?當(dāng)block不去截獲自動變量,其結(jié)構(gòu)體實例內(nèi)容不必在運行時才能確定秽浇,此時就會以單例形式存在數(shù)據(jù)區(qū)浮庐。而_NSConcreteMallocBlock正是block可以超出其作用域而存在的原因。舉個例子柬焕,block作為函數(shù)返回值返回時审残,棧上的block會因作用域結(jié)束而被釋放,故此時運行時會將block從棧上拷貝到堆上一份斑举,在這個過程中搅轿,堆上的block的ISA指針會被賦值_NSConcreteMallocBlock。這樣富玷,棧上的block廢棄后璧坟,我們依舊可以正確使用block。堆上的block的持有與釋放是服從引用計數(shù)的管理方式赎懦,和普通OC對象無異雀鹃。
在ARC下,多數(shù)情況編譯器可以自動判斷在合適的時候?qū)lock進行拷貝铲敛,例如像上面舉得例子褐澎。但是當(dāng)block作為方法的參數(shù)傳遞時,編譯器不會自動拷貝block伐蒋,我們需要自己判斷是否要對block對象調(diào)用copy方法工三,將棧上的block拷貝到堆上迁酸。例如以下情形拷貝就是必要的:
{
void(^blk1)() = ^() {NSLog(@"block 1");};
void(^blk2)() = ^() {NSLog(@"block 2");};
NSArray *array = [NSArray arrayWithObjects:[blk1 copy], [blk2 copy], nil];
return array;
}
不過如果在方法內(nèi)已經(jīng)對block進行了適當(dāng)?shù)膹?fù)制,那么傳遞的時候我們就不必在拷貝了俭正,比如cocoa框架中含有usingBlock的方法以及GCD中的方法奸鬓。
如果對已經(jīng)在堆上的block執(zhí)行copy方法,按照引用計數(shù)的管理方式掸读,block的引用計數(shù)會增加串远,而如果是靜態(tài)數(shù)據(jù)區(qū)的block,那么調(diào)用copy方法不會做任何事情儿惫,所以必要的時候執(zhí)行copy操作總是安全的澡罚。
__block變量的情況是類似的,當(dāng)棧上的block執(zhí)行copy操作時肾请,其所持有的棧上的__block變量也會隨之拷貝到堆上留搔,并被堆上的block所持有。從源碼可以知道铛铁,__block變量對應(yīng)的結(jié)構(gòu)體中有一指向自身結(jié)構(gòu)體的指針__forwarding隔显,當(dāng)__block變量拷貝到堆上時,堆上的__block變量的__forwarding指針指向自己饵逐,而棧上的__block變量的__forwarding指向堆上的__block變量結(jié)構(gòu)體括眠,故無論什么情況下,程序都可以正確找到__block變量對應(yīng)的結(jié)構(gòu)體倍权。因此當(dāng)棧上的block和__block變量被廢棄時掷豺,程序依舊可以使用堆上的block和__block變量,這就是block和__block變量可以超出作用域存在的原因薄声。
copy與dispose方法的作用
OC中的對象類型和id類型也可以被block所截獲萌业,這種情況下,在block對應(yīng)的結(jié)構(gòu)體實例中會有一個OC對象類型的成員變量奸柬,換句話說生年,OC對象成為了結(jié)構(gòu)體的成員變量,在內(nèi)存管理篇我們說過廓奕,OC對象類型不能作為結(jié)構(gòu)體的成員變量抱婉,因為編譯器無法正確判斷結(jié)構(gòu)體的持有和廢棄的生命周期,無法很好地管理OC對象的內(nèi)存桌粉,但是運行時可以準確把握block從椪艏ǎ拷貝到堆以及在堆上被廢棄的時機,因此這里OC對象也可以放入block的結(jié)構(gòu)體中铃肯。當(dāng)block從椈家冢拷貝到堆時,會調(diào)用copy方法,當(dāng)堆上的block不再被持有步藕,要被廢棄時惦界,調(diào)用dispose方法。
如果OC對象被指定為__block變量咙冗,那么和之前類似沾歪,會對應(yīng)生成一個__block變量的結(jié)構(gòu)體,結(jié)構(gòu)體中會保存OC對象雾消,同時還會有該對象自己的copy和dispose方法灾搏,當(dāng)block被拷貝到堆時,賦值給__block變量的OC對象會通過此copy方法被持有立润,而當(dāng)堆上的block要被廢棄時狂窑,__block變量持有的對象也應(yīng)該釋放,這時會通過此dispose方法進行釋放桑腮,這和block本身的copy和dispose邏輯是一樣的蕾域。
PS:關(guān)于block引起循環(huán)引用的問題,在內(nèi)存管理篇已經(jīng)提及到旦,這里不再詳述了。
更新@20170624
關(guān)于目前ARC下蘋果對于block的處理方式巨缘,事實上添忘,被__strong修飾的block在棧上生成后運行時會自動將其拷貝到堆上,并非前文所說只是在必要的時候才將block從椚羲拷貝到堆搁骑,即便是block僅在某個函數(shù)內(nèi)使用,不超出函數(shù)作用域又固。而被__weak修飾的block則在棧上生成后依舊會保持在棧上仲器。