Block語(yǔ)法簡(jiǎn)介
Block:可以理解為帶有自動(dòng)變量值的匿名函數(shù)。
Blocks提供了類似由C++和Objective-C類生成實(shí)例變量或?qū)ο髞?lái)保持變量值的方法。
Block語(yǔ)法定義
^返回值 類型參數(shù)列表 表達(dá)式
^void (int event) { NSLog(@"。。。");}
Block類型變量定義
int (^blk)(int); 可以認(rèn)為是匿名函數(shù)的地址,但是實(shí)際上它是是被看成對(duì)象來(lái)操作的,有自己的isa指針。
簡(jiǎn)單的Block原理分析
我們來(lái)分析最簡(jiǎn)單的block:我們定了一個(gè)變量名稱為blk的Block變量,在定義部分省略了返回值和類型參數(shù)列表,然后在下面調(diào)用它,打出一串”Block”;
void (^blk)(void) = ^{printf("Block\n");};
blk();
源碼通過(guò)clang,去掉一些類型轉(zhuǎn)換我們可以得到以下代碼
struct _block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
//Block定義的結(jié)構(gòu)體
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;
}
};
//我們的Block里面的函數(shù)
static void __main_block_func_0(struct __main_block_impl_0 *_cself)
{
printf("Block\n");
}
//存儲(chǔ)block的其他信息,大小等
struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __mainBlock_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
/*以下是我們的代碼部分*/
//賦值部分,
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__mainBlock_desc_0_DATA);
struct __main_block_impl_0 *blk = &temp;
//調(diào)用部分
(*blk->impl.FuncPtr)(blk);
/*以下是我們的代碼部分*/
看起來(lái)好像很麻煩?居然兩句代碼變出了這么多代碼。慢慢分析起來(lái)其實(shí)也不難理解
C++中,struct 約等于 class,唯一差別是struct中的默認(rèn)成員屬性是public的。class中的默認(rèn)成員屬性是private的。所以struct也可以擁有變量和函數(shù)。
首先系統(tǒng)自動(dòng)給我們生成了三個(gè)結(jié)構(gòu)體。
//block的結(jié)構(gòu)體定義
struct __main_block_impl_0 {
struct _block_impl impl;//Block isa ,函數(shù)地址等定義
struct __main_block_desc_0 *Desc;//Block size等信息定義
};
struct _block_impl {
void *isa;//所屬的類
int Flags;
int Reserved;
void *FuncPtr;//函數(shù)地址
};
struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
}
生成了兩個(gè)函數(shù)
//Block信息初始化的函數(shù)
__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里面的函數(shù),_cself就是調(diào)用這個(gè)函數(shù)的調(diào)用者的指針
static void __main_block_func_0(struct __main_block_impl_0 *_cself)
{
printf("Block\n");
}
我們定義Block的代碼如下
void (^blk)(void) = ^{printf("Block\n");};
轉(zhuǎn)化成
/*
初始化
__main_block_func_0:函數(shù)地址,
__mainBlock_desc_0_DATA:block的size信息
*/
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__mainBlock_desc_0_DATA);
struct __main_block_impl_0 *blk = &temp;
定義了一個(gè)main_block_impl_0的block,初始化函數(shù)為main_block_impl_0,傳入函數(shù)指針和block的大小等信息
//Block信息初始化的函數(shù)
__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;
}
我們看到這個(gè)block的class類型為_(kāi)NSConcreteStackBlock,這個(gè)下面會(huì)詳細(xì)解釋。函數(shù)地址為FuncPtr。所以iOS里的Block是被當(dāng)成一個(gè)類來(lái)看待的,有自己的存儲(chǔ)空間??梢岳斫鉃?strong>帶有自動(dòng)變量值的匿名函數(shù)
我們調(diào)用block的代碼如下
//調(diào)用部分,
blk();
轉(zhuǎn)化成
//調(diào)用部分
(*blk->impl.FuncPtr)(blk);
拿到上面定義的Block變量blk,找到函數(shù)地址,調(diào)用函數(shù),并把調(diào)用者也就是blk傳遞進(jìn)去。
Block會(huì)截獲自動(dòng)變量
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt,val);};
val = 2;
fmt = "these values were changed. val = %d\n";
blk();
輸出為輸出
val = 10
而不是
these values were changed. val = 2
說(shuō)明自動(dòng)變量截獲只能保存執(zhí)行block語(yǔ)法瞬間的值
但我們知道加上__block,是可以在Block內(nèi)部對(duì)變量進(jìn)行修改的。詳細(xì)講__block(__block storage-class-specifier)為存儲(chǔ)類型說(shuō)明符,
c語(yǔ)言有以下說(shuō)明符:
- tydedef
- extern
- static:表示靜態(tài)變量存儲(chǔ)在數(shù)據(jù)區(qū)
- auto:表示自動(dòng)變量存儲(chǔ)在棧
- register:應(yīng)將其保存在CPU的寄存器中(而不是棧或堆)
__block類似于后三種,表示將變量值設(shè)置到哪個(gè)存儲(chǔ)區(qū)
如果我們加上__block
__block int val = 10;
void (^blk)(void) = ^{val=1;};
進(jìn)行編譯后,并剔除和以上通過(guò)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;
}
我們發(fā)現(xiàn)val變量居然變成了結(jié)構(gòu)體實(shí)例__Block_byref_val_0,既在棧上生成了__Block_byref_val_0結(jié)構(gòu)體實(shí)例,且初始化為10
而^{val=1;}賦值過(guò)程變成什么樣子了呢
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val;
(val->__forwarding->val) = 1;
}
找到Block下面的val變量,拿出val變量的__forwarding指向的val變量,拿出val變量下的val值賦值。如果Block此時(shí)在棧區(qū),那么__forwarding指向val變量自己。

copy到堆后:__forwarding指向堆區(qū)的val變量

Block的類型
- _NSConcreteStackBlock: 存儲(chǔ)在棧區(qū),需要截取變量
- _NSConcreteGlobalBlock: 1.存儲(chǔ)在程序數(shù)據(jù)區(qū)域,2.不需要截取變量
- _NSConcreteMallocBlock: 存儲(chǔ)在堆區(qū)
根據(jù)之前的分析,我們看到block的isa指針為_(kāi)NSConcreteStackBlock,里面有個(gè)Stack,可以猜到,這個(gè)為存儲(chǔ)在棧區(qū)的Block。
我們?cè)谟浭鋈肿兞康牡胤绞褂肂lock語(yǔ)法時(shí)候,生成的Block為_(kāi)NSConcreteGlobalBlock,因?yàn)樵谑褂萌肿兞康牡胤讲荒苁褂米詣?dòng)變量,所以不存在對(duì)自動(dòng)變量的截獲。
void (^blk)(void) = ^{printf("global Block");};
int main(){
}
那如何使得棧上的Block到堆上呢?
ARC 條件下編譯器會(huì)適當(dāng)判斷,自動(dòng)生成將block從棧上復(fù)制到堆上的代碼。
//比如
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count){return rate * count;};
}
該代碼返回設(shè)置在棧上的Block函數(shù)。但函數(shù)作用域結(jié)束,棧上的Block被廢棄。但編譯器自動(dòng)會(huì)加上copy
什么情況下編譯器不能進(jìn)行判斷要不要加copy,而需要手動(dòng)執(zhí)行copy?
- 向方法或者函數(shù)參數(shù)中傳遞block;
- 如果在函數(shù)或者方法中已經(jīng)copy了傳遞過(guò)來(lái)的參數(shù)(Cocoa框架的方法且方法名中含有usingBlock,GCD的API)
例:
在用 如NSArray 的 enumerateObjectsUsingBlock 的實(shí)例方法和 dispatch_async函數(shù)前,不用手動(dòng)copy。
在用如NSArray的 initWithObjects 前,需要手動(dòng)copy。
typedef void (^blk_t)(void);
NSArray *blocks = [self getBlockArray];
blk_t blk = (blk_t)[blocks objectAtIndex:0];
blk();
- (NSArray *)getBlockArray {
int val = 0;
return [NSArray arrayWithObjects: ^{NSLog(@"blk0:%d",val);},
^{NSLog(@"blk0:%d",val);}, nil];
}
//會(huì)發(fā)生崩潰。因?yàn)镹SArray 的initWithObjects因?yàn)橄到y(tǒng)不確定加入的是不是block,不會(huì)自動(dòng)執(zhí)行copy操作,如果我們也不執(zhí)行,在作用域外調(diào)用就會(huì)發(fā)生崩潰。
也許你會(huì)想,那么任何時(shí)候都用copy就好啦。但是從棧上的block copy到堆上很耗CPU。所以最好自己判斷需不需要把Blockcopy到棧上
綜上:我們要想把棧上的Block復(fù)制到堆上,只有執(zhí)行copy方法,有些情況下,系統(tǒng)會(huì)自動(dòng)幫我們執(zhí)行,但也有些情況我們需要手動(dòng)執(zhí)行copy。
棧上的Block被復(fù)制到堆的情況
- 手動(dòng)調(diào)用Block的copy實(shí)例方法
- Block作為函數(shù)返回值返回
- 將block賦值給附有__strong修飾符id類型的類或Block類型的成員變量。
- 如果在函數(shù)或者方法中已經(jīng)copy了傳遞過(guò)來(lái)的參數(shù)(Cocoa框架的方法且方法名中含有usingBlock,GCD的API)
注: __weak, __strong 用來(lái)修飾變量,此外還有 __unsafe_unretained, __autoreleasing 都是用來(lái)修飾變量的。
__strong 是缺省的關(guān)鍵詞。
__weak 聲明了一個(gè)可以自動(dòng) nil 化的弱引用。
__unsafe_unretained 聲明一個(gè)弱應(yīng)用,但是不會(huì)自動(dòng)nil化,也就是說(shuō),如果所指向的內(nèi)存區(qū)域被釋放了,這個(gè)指針就是一個(gè)野指針了。
__autoreleasing 用來(lái)修飾一個(gè)函數(shù)的參數(shù),這個(gè)參數(shù)會(huì)在函數(shù)返回的時(shí)候被自動(dòng)釋放。
各種類型的Block調(diào)用copy后
| Block類型 | 存儲(chǔ)區(qū)域 | 賦值效果 |
|---|---|---|
| _NSConcreteStackBlock | 棧 | 棧-》堆 |
| _NSConcreteGlobalBlock | 程序數(shù)據(jù)區(qū)域 | 什么也不做 |
| _NSConcreteMallocBlock | 堆 | 引用計(jì)數(shù)+1 |
所以不管任何時(shí)候copy方法復(fù)制都不會(huì)出錯(cuò)。但是多次調(diào)用copy會(huì)不會(huì)引起內(nèi)存釋放問(wèn)題呢?
//多次調(diào)用copy
blk = [[[[blk copy] copy] copy] copy];
//代碼解釋
{
/*
將配置在棧上的Block賦值給blk變量。
*/
blk_t temp = [blk copy];
/*
將配置在堆上的block賦值給tmp變量,temp強(qiáng)持有Block
*/
blk = temp;
/*
將變量tmp的Block賦值為變量blk,blk強(qiáng)持有Block
此時(shí)block的持有者為變量temp和blk;
*/
}
/*
由于變量作用域結(jié)束,所以變量temp被廢棄,其強(qiáng)引用失效并釋放所持有的Block
由于Block的此時(shí)還被blk持有,所以沒(méi)有廢棄。
*/
{
/*
配置在堆上的Block被賦值給blk;同時(shí)變量blk持有強(qiáng)制引用的Block
*/
blk_t temp = [blk copy];
/*
將配置在堆上的block賦值給tmp變量,temp強(qiáng)持有Block
*/
blk = temp;
/*
將變量tmp的Block賦值為變量blk,blk強(qiáng)持有Block
此時(shí)block的持有者為變量temp和blk;
*/
}
/*
由于變量作用域結(jié)束,所以變量temp被廢棄,其強(qiáng)引用失效并釋放所持有的Block
由于Block的此時(shí)還被blk持有,所以沒(méi)有廢棄。
*/
/*下面重復(fù)*/
答案是 :多次調(diào)用copy完全不會(huì)有任何問(wèn)題
一個(gè)含有__block變量的block被copy
| __block變量的配置存儲(chǔ)域 | Block從棧賦值到堆時(shí)候的影響 |
|---|---|
| 棧 | 從棧賦值到堆并被Block持有 |
| 堆 | 被Block持有 |
我們看看以下代碼,一個(gè)在棧上的Block
__block int val = 0;
void (^blk)(void) = ^{val = 1; printf("val = %d\n",val);};
blk();
printf("val = %d\n",val);
同樣的如果Block在堆上兩個(gè)輸出也一樣:
說(shuō)明
無(wú)論在Block語(yǔ)法中,Block語(yǔ)法外使用__block變量,還是__block變量配置在棧上或者堆上,都可以順利訪問(wèn)同一個(gè)__block
說(shuō)到Block不得不談循環(huán)引用問(wèn)題,但是比較簡(jiǎn)單,網(wǎng)上一大堆,這里也不分析了。
小結(jié)
本文探索了Block的底層實(shí)現(xiàn)機(jī)制,我們發(fā)現(xiàn)Block在iOS中是作為對(duì)象來(lái)管理的?,F(xiàn)在再看看這句話
Block:可以理解為帶有自動(dòng)變量值的匿名函數(shù)。是不是形容的很貼切。