Block語法簡介
Block:可以理解為帶有自動變量值的匿名函數(shù)。
Blocks提供了類似由C++和Objective-C類生成實(shí)例變量或?qū)ο髞肀3肿兞恐档姆椒ā?/p>
Block語法定義
^返回值
類型參數(shù)列表
表達(dá)式
^void (int event) { NSLog(@"肉盹。看峻。隘弊。");}
Block類型變量定義
int (^blk)(int); 可以認(rèn)為是匿名函數(shù)的地址落恼,但是實(shí)際上它是是被看成對象來操作的撒顿,有自己的isa指針关霸。
簡單的Block原理分析
我們來分析最簡單的block:我們定了一個變量名稱為blk的Block變量传黄,在定義部分省略了返回值和類型參數(shù)列表,然后在下面調(diào)用它队寇,打出一串”Block”;
void (^blk)(void) = ^{printf("Block\n");};
blk();
源碼通過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");
}
//存儲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);
/*以下是我們的代碼部分*/
看起來好像很麻煩识埋?居然兩句代碼變出了這么多代碼。慢慢分析起來其實(shí)也不難理解
C++中零渐,struct 約等于 class,唯一差別是struct中的默認(rèn)成員屬性是public的窒舟。class中的默認(rèn)成員屬性是private的。所以struct也可以擁有變量和函數(shù)相恃。
首先系統(tǒng)自動給我們生成了三個結(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;
}
生成了兩個函數(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)用這個函數(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;
定義了一個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;
}
我們看到這個block的class類型為_NSConcreteStackBlock,這個下面會詳細(xì)解釋扫俺。函數(shù)地址為FuncPtr苍苞。所以iOS里的Block是被當(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會截獲自動變量
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
說明自動變量截獲只能保存執(zhí)行block語法瞬間的值
但我們知道加上__block盈简,是可以在Block內(nèi)部對變量進(jìn)行修改的凑耻。詳細(xì)講__block(__block storage-class-specifier)為存儲類型說明符,
c語言有以下說明符:
- tydedef
- extern
- static:表示靜態(tài)變量存儲在數(shù)據(jù)區(qū)
- auto:表示自動變量存儲在棧
- register:應(yīng)將其保存在CPU的寄存器中(而不是椖停或堆)
__block類似于后三種香浩,表示將變量值設(shè)置到哪個存儲區(qū)
如果我們加上__block
__block int val = 10;
void (^blk)(void) = ^{val=1;};
進(jìn)行編譯后,并剔除和以上通過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;}
賦值過程變成什么樣子了呢
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: 存儲在棧區(qū),需要截取變量
- _NSConcreteGlobalBlock: 1.存儲在程序數(shù)據(jù)區(qū)域恩闻,2.不需要截取變量
- _NSConcreteMallocBlock: 存儲在堆區(qū)
根據(jù)之前的分析,我們看到block的isa指針為_NSConcreteStackBlock剧董,里面有個Stack幢尚,可以猜到,這個為存儲在棧區(qū)的Block翅楼。
我們在記述全局變量的地方使用Block語法時(shí)候尉剩,生成的Block為_NSConcreteGlobalBlock,因?yàn)樵谑褂萌肿兞康牡胤讲荒苁褂米詣幼兞浚圆淮嬖趯ψ詣幼兞康慕孬@毅臊。
void (^blk)(void) = ^{printf("global Block");};
int main(){
}
那如何使得棧上的Block到堆上呢理茎?
ARC 條件下編譯器會適當(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被廢棄。但編譯器自動會加上copy
什么情況下編譯器不能進(jìn)行判斷要不要加copy,而需要手動執(zhí)行copy础倍?
- 向方法或者函數(shù)參數(shù)中傳遞block烛占;
- 如果在函數(shù)或者方法中已經(jīng)copy了傳遞過來的參數(shù)(Cocoa框架的方法且方法名中含有usingBlock,GCD的API)
例:
在用 如NSArray 的 enumerateObjectsUsingBlock 的實(shí)例方法和 dispatch_async函數(shù)前沟启,不用手動copy忆家。
在用如NSArray的 initWithObjects 前,需要手動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];
}
//會發(fā)生崩潰芽卿。因?yàn)镹SArray 的initWithObjects因?yàn)橄到y(tǒng)不確定加入的是不是block,不會自動執(zhí)行copy操作胳搞,如果我們也不執(zhí)行卸例,在作用域外調(diào)用就會發(fā)生崩潰。
也許你會想流酬,那么任何時(shí)候都用copy就好啦币厕。但是從棧上的block copy到堆上很耗CPU。所以最好自己判斷需不需要把Blockcopy到棧上
綜上:我們要想把棧上的Block復(fù)制到堆上芽腾,只有執(zhí)行copy方法旦装,有些情況下,系統(tǒng)會自動幫我們執(zhí)行摊滔,但也有些情況我們需要手動執(zhí)行copy阴绢。
棧上的Block被復(fù)制到堆的情況
- 手動調(diào)用Block的copy實(shí)例方法
- Block作為函數(shù)返回值返回
- 將block賦值給附有__strong修飾符id類型的類或Block類型的成員變量。
- 如果在函數(shù)或者方法中已經(jīng)copy了傳遞過來的參數(shù)(Cocoa框架的方法且方法名中含有usingBlock艰躺,GCD的API)
注: __weak, __strong 用來修飾變量呻袭,此外還有 __unsafe_unretained, __autoreleasing 都是用來修飾變量的。
__strong 是缺省的關(guān)鍵詞腺兴。
__weak 聲明了一個可以自動 nil 化的弱引用左电。
__unsafe_unretained 聲明一個弱應(yīng)用,但是不會自動nil化页响,也就是說篓足,如果所指向的內(nèi)存區(qū)域被釋放了,這個指針就是一個野指針了闰蚕。
__autoreleasing 用來修飾一個函數(shù)的參數(shù)栈拖,這個參數(shù)會在函數(shù)返回的時(shí)候被自動釋放。
各種類型的Block調(diào)用copy后
Block類型 | 存儲區(qū)域 | 賦值效果 |
---|---|---|
_NSConcreteStackBlock | 棧 | 棧-》堆 |
_NSConcreteGlobalBlock | 程序數(shù)據(jù)區(qū)域 | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用計(jì)數(shù)+1 |
所以不管任何時(shí)候copy方法復(fù)制都不會出錯没陡。但是多次調(diào)用copy會不會引起內(nèi)存釋放問題呢涩哟?
//多次調(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持有砾赔,所以沒有廢棄。
*/
{
/*
配置在堆上的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持有弹沽,所以沒有廢棄檀夹。
*/
/*下面重復(fù)*/
答案是 :多次調(diào)用copy完全不會有任何問題
一個含有__block變量的block被copy
__block變量的配置存儲域 | Block從棧賦值到堆時(shí)候的影響 |
---|---|
棧 | 從棧賦值到堆并被Block持有 |
堆 | 被Block持有 |
我們看看以下代碼,一個在棧上的Block
__block int val = 0;
void (^blk)(void) = ^{val = 1; printf("val = %d\n",val);};
blk();
printf("val = %d\n",val);
同樣的如果Block在堆上兩個輸出也一樣:
說明
無論在Block語法中策橘,Block語法外使用__block變量炸渡,還是__block變量配置在棧上或者堆上,都可以順利訪問同一個__block
說到Block不得不談循環(huán)引用問題丽已,但是比較簡單蚌堵,網(wǎng)上一大堆,這里也不分析了沛婴。
小結(jié)
本文探索了Block的底層實(shí)現(xiàn)機(jī)制吼畏,我們發(fā)現(xiàn)Block在iOS中是作為對象來管理的。現(xiàn)在再看看這句話
Block:可以理解為帶有自動變量值的匿名函數(shù)嘁灯。是不是形容的很貼切泻蚊。