通过前几章的学习我们可以知道,Block转换为Block的结构体类型
的自动变量
,__block变量
转换为__block变量
的结构体类型
的自动变量,所谓结构体类型
的自动变量
,即栈
上生成的该结构体的实例
。
Block存储域
之前我们在讲Block本质的时候知道,Block也是Objective-C
对象,其isa
指向了NSConcreteStackBlock
类,还有两个与此类似的类分别是NSConcreteGlobalBlock
和NSConcreteMallocBlock
。
NSConcreteStackBlock
类的名称中含有“栈”(stack)
一词,即该类的对象设置在栈
上,同样的NSConcreteGlobalBlock
类对象与全局变量
一样设置在数据区域
中,NSConcreteMallocBlock
类对象则设置在由malloc
函数分配的内存块(堆)
中。
前几篇文章中我们介绍的例子都是NSConcreteStackBlock类,且都设置在栈上,在记叙全局变量的地方使用Block语法时,生成的Block为NSConcreteGlobalBlock类,例如:
void (^blk)(void) = ^{printf("Global Block\n");};
int main(){
}
代码转换如下:
struct __blk_block_impl_0 {
struct __block_impl impl;
struct __blk_block_desc_0* Desc;
__blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __blk_block_func_0(struct __blk_block_impl_0 *__cself) {
printf("Global Block\n");}
static struct __blk_block_desc_0 {
size_t reserved;
size_t Block_size;
} __blk_block_desc_0_DATA = { 0, sizeof(struct __blk_block_impl_0)};
static __blk_block_impl_0 __global_blk_block_impl_0((void *)__blk_block_func_0, &__blk_block_desc_0_DATA);
void (*blk)(void) = ((void (*)())&__global_blk_block_impl_0);
int main(){
}
该Block的类为_NSConcreteGlobalBlock
类,设置在程序的数据区域
中,在使用全局变量
的地方不能使用自动变量
,所以也就不存在对自动变量
进行截获
,由此Block结构体实例
的内容不依赖于执行时
的状态,所以整个程序中只需要一个实例。因此Block即被设置在于全局变量相同的数据区域
中。
在记叙全局变量
的地方有Block语法
时以及Block语法
的表达式
不使用应截获的自动变量
时Block为_NSConcreteGlobalBlock
类对象,除此之外Block语法
生成的Block为_NSConcreteStackBlock类对象
,且设置在栈
上。
那么NSConcreteMallocBlock
类在何时使用呢?
从栈复制到堆
之前一篇我们提到过,由Block语法
生成的值Block上可以存有超过
其变量作用域
的被截获
的自动变量
,当变量作用域结束
时,原来的自动变量被废弃
,该Block就被废弃,将不能通过指针
来访问原来的自动变量
。由于__block变量
也配置在栈
上,如果其所属的变量作用域结束
,该__block变量
也会被废弃。
Block会提供将Block
和__block变量
从栈
上复制到堆
上的方法来解决这个问题。将配置在栈
上的Block复制到堆
上,这样即使Block语法
记述的变量作用域结束
,堆
上的Block还可以继续存在。
复制到堆
上的Block将_ NSConcreteMallocBlock类对象
写入Block的结构体实例
的成员变量isa
。
impl.isa = &_ NSConcreteMallocBlock;
而__block变量
用结构体成员变量__forwarding
可以实现无论__block变量
配置在栈
上还是堆
上时都能够正确的访问__block变量
。
那么Blocks提供的方法是什么呢?实际上当ARC有效时
,大多数
情形下编译器
会恰当的进行判断,自动生成
将Block从栈上复制到堆上的代码。我们就来举例说明。
typedef void (^block_t)(void);
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
block_t block = [self func];
block();
}
-(block_t)func{
__block int add=10;
return ^{NSLog(@"add=%d\n",++add);};
};
@end
该源代码为返回配置在栈
上的Block函数
。即程序执行中从函数返回函数调用方时
变量作用域结束
,因此栈
上的Block也被废弃。但是该源代码可以正常运行成功输出如下。
2018-10-26 13:43:34.257219+0800 testblock[43867:4966419] add=11
这是因为在ARC有效
时编译器会如下转换代码:
block_t block = &__ViewController__func_block_impl_0(__ViewController__func_block_func_0, &__ViewController__func_block_desc_0_DATA, (__Block_byref_add_0 *)&add, 570425344)));
block = objc_retainBlock(block);
return objc_autoreleaseReturnValue(block);
objc_retainBlock
函数实际上就是Block_copy
函数。即:
//通过Block语法生成的Block,配置在栈上的Block结构体实例,赋值给相当于Block类型的变量block中
block = _Block_copy(block);
//_Block_copy函数将栈上的Block复制到堆上,复制后将堆上的地址作为指针赋值给变量block
return objc_autoreleaseReturnValue(block);
//将堆上的Block作为OC对象注册到autoreleasepool中,然后返回对象
将Block作为函数返回值
时,编译器会自动
生成复制到堆上的代码。
之前说大多数情况
下编译器会适当的判断,在此之外的情况下需要手动
生成代码,将Block从栈复制到堆上
。此时我们使用“copy实例方法”
。
那么编译器不能
进行判断的情况又是什么样呢。
1.向方法或函数的参数
中传递Block时
2.Cocoa框架
的方法且方法名字中含有usingBlock
等时
3.GCD
的API
我们再来举例说明:
typedef void (^block_t)(void);
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
id obj = [self getBlockArray];
block_t block = (block_t)[obj objectAtIndex:0];
block();
}
- (id)getBlockArray{
int val = 10;
return [[NSArray alloc] initWithObjects:^{NSLog(@"block0:%d",val);},^{NSLog(@"block1:%d",val);}, nil];
}
@end
getBlockArray
方法在栈
上生成两个Block,并传递给NSArray
类的initWithObjects
实例方法。但是在执行时会出现异常。
2018-10-26 14:12:16.621722+0800 testblock[44061:5009777] block0:10
objc[44061]: Attempt to use unknown class 0x600001e6b680.
可以看到
栈
上的block为null
,这是因为栈
上的Block已经被废弃
。可惜此时编译器不能
判断是否需要复制,我们就需要手动复制,但是将Block从栈
复制到堆
是相当消耗CPU的,因此只有在必须复制的时候我们再进行手动复制。
- (id)getBlockArray{
int val = 10;
return [[NSArray alloc] initWithObjects:[^{NSLog(@"block0:%d",val);} copy],[^{NSLog(@"block1:%d",val);} copy], nil];
}
像这样我们的程序就可以正常的运行了。
__block变量存储域
上面我们把重点放在Block进行说明,那么对__block变量
又是如何处理的呢?使用__block变量
的Block从栈复制到堆上
时,__block变量
也会受到影响。
若在一个Block中使用__block变量
,则当该Block从栈复制到堆
时,使用的
所有__block变量
也必定配置在栈
上,这些__block变量
也会跟随Block一起复制到堆
,此时Block持有__block变量
。
现在我们来看看之前说过的__forwarding成员变量
,“不管__block变量
配置在栈
上还是在堆
上,都能够正确地访问该变量”。通过Block的复制,__block变量
也从栈
复制到堆
,此时可同时
访问栈
上的__block变量
和堆
上的__block变量
。
__block int val = 0;
void (^blk)(void) = [^{++val;} copy];
++val;
blk();
NSLog(@"%d",val);
利用copy方法复制了__block变量的Block语法。Block和__block变量两者均是从栈复制到堆。
//该val为复制到堆上的__block变量
void (^blk)(void) = [^{++val;} copy];
//该val为复制前栈上的__block变量
++val;
//以上两者都可转化为如下形式
++(val.__forwarding->val);
但是栈
上的__block变量
在从栈
复制到堆
时,会将成员变量forwarding
的值替换为复制目标堆
上的__block变量的地址
。通过这个功能,无论是在Block中,Block外使用__block变量
,堆
上或者栈
上的__block变量
,都可以顺利的访问同一个__block变量
。