Block语法简介
Block:可以理解为带有自动变量值的匿名函数。
Blocks提供了类似由C++和Objective-C类生成实例变量或对象来保持变量值的方法。
Block语法定义
^返回值
类型参数列表
表达式
^void (int event) { NSLog(@"。。。");}
Block类型变量定义
int (^blk)(int); 可以认为是匿名函数的地址,但是实际上它是是被看成对象来操作的,有自己的isa指针。
简单的Block原理分析
我们来分析最简单的block:我们定了一个变量名称为blk的Block变量,在定义部分省略了返回值和类型参数列表,然后在下面调用它,打出一串”Block”;
void (^blk)(void) = ^{printf("Block\n");};
blk();
源码通过clang,去掉一些类型转换我们可以得到以下代码
struct _block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
//Block定义的结构体
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里面的函数
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;
//调用部分
(*blk->impl.FuncPtr)(blk);
/*以下是我们的代码部分*/
看起来好像很麻烦?居然两句代码变出了这么多代码。慢慢分析起来其实也不难理解
C++中,struct 约等于 class,唯一差别是struct中的默认成员属性是public的。class中的默认成员属性是private的。所以struct也可以拥有变量和函数。
首先系统自动给我们生成了三个结构体。
//block的结构体定义
struct __main_block_impl_0 {
struct _block_impl impl;//Block isa ,函数地址等定义
struct __main_block_desc_0 *Desc;//Block size等信息定义
};
struct _block_impl {
void *isa;//所属的类
int Flags;
int Reserved;
void *FuncPtr;//函数地址
};
struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
}
生成了两个函数
//Block信息初始化的函数
__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里面的函数,_cself就是调用这个函数的调用者的指针
static void __main_block_func_0(struct __main_block_impl_0 *_cself)
{
printf("Block\n");
}
我们定义Block的代码如下
void (^blk)(void) = ^{printf("Block\n");};
转化成
/*
初始化
__main_block_func_0:函数地址,
__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,初始化函数为main_block_impl_0,传入函数指针和block的大小等信息
//Block信息初始化的函数
__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,这个下面会详细解释。函数地址为FuncPtr。所以iOS里的Block是被当成一个类来看待的,有自己的存储空间。可以理解为带有自动变量值的匿名函数
我们调用block的代码如下
//调用部分,
blk();
转化成
//调用部分
(*blk->impl.FuncPtr)(blk);
拿到上面定义的Block变量blk,找到函数地址,调用函数,并把调用者也就是blk传递进去。
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
说明自动变量截获只能保存执行block语法瞬间的值
但我们知道加上__block,是可以在Block内部对变量进行修改的。详细讲__block(__block storage-class-specifier)为存储类型说明符,
c语言有以下说明符:
- tydedef
- extern
- static:表示静态变量存储在数据区
- auto:表示自动变量存储在栈
- register:应将其保存在CPU的寄存器中(而不是栈或堆)
__block类似于后三种,表示将变量值设置到哪个存储区
如果我们加上__block
__block int val = 10;
void (^blk)(void) = ^{val=1;};
进行编译后,并剔除和以上通过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;
}
我们发现val变量居然变成了结构体实例__Block_byref_val_0,既在栈上生成了__Block_byref_val_0结构体实例,且初始化为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此时在栈区,那么__forwarding指向val变量自己。
copy到堆后:__forwarding指向堆区的val变量
Block的类型
- _NSConcreteStackBlock: 存储在栈区,需要截取变量
- _NSConcreteGlobalBlock: 1.存储在程序数据区域,2.不需要截取变量
- _NSConcreteMallocBlock: 存储在堆区
根据之前的分析,我们看到block的isa指针为_NSConcreteStackBlock,里面有个Stack,可以猜到,这个为存储在栈区的Block。
我们在记述全局变量的地方使用Block语法时候,生成的Block为_NSConcreteGlobalBlock,因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量的截获。
void (^blk)(void) = ^{printf("global Block");};
int main(){
}
那如何使得栈上的Block到堆上呢?
ARC 条件下编译器会适当判断,自动生成将block从栈上复制到堆上的代码。
//比如
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count){return rate * count;};
}
该代码返回设置在栈上的Block函数。但函数作用域结束,栈上的Block被废弃。但编译器自动会加上copy
什么情况下编译器不能进行判断要不要加copy,而需要手动执行copy?
- 向方法或者函数参数中传递block;
- 如果在函数或者方法中已经copy了传递过来的参数(Cocoa框架的方法且方法名中含有usingBlock,GCD的API)
例:
在用 如NSArray 的 enumerateObjectsUsingBlock 的实例方法和 dispatch_async函数前,不用手动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];
}
//会发生崩溃。因为NSArray 的initWithObjects因为系统不确定加入的是不是block,不会自动执行copy操作,如果我们也不执行,在作用域外调用就会发生崩溃。
也许你会想,那么任何时候都用copy就好啦。但是从栈上的block copy到堆上很耗CPU。所以最好自己判断需不需要把Blockcopy到栈上
综上:我们要想把栈上的Block复制到堆上,只有执行copy方法,有些情况下,系统会自动帮我们执行,但也有些情况我们需要手动执行copy。
栈上的Block被复制到堆的情况
- 手动调用Block的copy实例方法
- Block作为函数返回值返回
- 将block赋值给附有__strong修饰符id类型的类或Block类型的成员变量。
- 如果在函数或者方法中已经copy了传递过来的参数(Cocoa框架的方法且方法名中含有usingBlock,GCD的API)
注: __weak, __strong 用来修饰变量,此外还有 __unsafe_unretained, __autoreleasing 都是用来修饰变量的。
__strong 是缺省的关键词。
__weak 声明了一个可以自动 nil 化的弱引用。
__unsafe_unretained 声明一个弱应用,但是不会自动nil化,也就是说,如果所指向的内存区域被释放了,这个指针就是一个野指针了。
__autoreleasing 用来修饰一个函数的参数,这个参数会在函数返回的时候被自动释放。
各种类型的Block调用copy后
Block类型 | 存储区域 | 赋值效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 栈-》堆 |
_NSConcreteGlobalBlock | 程序数据区域 | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用计数+1 |
所以不管任何时候copy方法复制都不会出错。但是多次调用copy会不会引起内存释放问题呢?
//多次调用copy
blk = [[[[blk copy] copy] copy] copy];
//代码解释
{
/*
将配置在栈上的Block赋值给blk变量。
*/
blk_t temp = [blk copy];
/*
将配置在堆上的block赋值给tmp变量,temp强持有Block
*/
blk = temp;
/*
将变量tmp的Block赋值为变量blk,blk强持有Block
此时block的持有者为变量temp和blk;
*/
}
/*
由于变量作用域结束,所以变量temp被废弃,其强引用失效并释放所持有的Block
由于Block的此时还被blk持有,所以没有废弃。
*/
{
/*
配置在堆上的Block被赋值给blk;同时变量blk持有强制引用的Block
*/
blk_t temp = [blk copy];
/*
将配置在堆上的block赋值给tmp变量,temp强持有Block
*/
blk = temp;
/*
将变量tmp的Block赋值为变量blk,blk强持有Block
此时block的持有者为变量temp和blk;
*/
}
/*
由于变量作用域结束,所以变量temp被废弃,其强引用失效并释放所持有的Block
由于Block的此时还被blk持有,所以没有废弃。
*/
/*下面重复*/
答案是 :多次调用copy完全不会有任何问题
一个含有__block变量的block被copy
__block变量的配置存储域 | Block从栈赋值到堆时候的影响 |
---|---|
栈 | 从栈赋值到堆并被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不得不谈循环引用问题,但是比较简单,网上一大堆,这里也不分析了。
小结
本文探索了Block的底层实现机制,我们发现Block在iOS中是作为对象来管理的。现在再看看这句话
Block:可以理解为带有自动变量值的匿名函数。是不是形容的很贴切。