Blocks的存储域
Block和__block变量的实质就是 在栈上的结构体实例。 如:__block变量实质是 栈上__block变量的结构体实例。
其中Block也是oc的对象,该OC的类为:_NSConcreteStackBlock。 虽然该类并没有出现在源码,但是有很多与之类似的类:
_NSConcreteStackBlock 它的对象Block设置在栈上
_NSConcreteGlobalBlock 他与全局变量一样,设置在程序的数据区域(.data区)中。
_NSConcreteMallocBlock 它的对象设置在由malloc函数分配的内存块中(堆)
如图:
前面我们看到的是stack,设置在栈上。
但是例如:
void (^blk)(void) = ^{printf("sdfs");};
int main() ****
这里初始化的时候用的是global:
impl.isa = &_NSConcreteGlobalBlock;
此时,Block用结构体实例设置在程序的数据区域中。 因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。
Block用结构体实例的内容不依赖于执行时的状态,所以整个程序中只需要一个实例。因此将Block用结构体实例设置在与全局变量相同的数据区域中即可。
只在截获自动变量时,Block用结构体实例截获的值才会根据执行时的状态变化。
只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。
在以下情况Block为_NSConcreteGlobalBlock类对象:
1、记述全局变量的地方有Block语法时
2、Block语法的表达式中不使用应截获的自动变量时
除此之外的Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。
还有就是设置在堆上的情况:
1、Block超出变量作用域可存在的原因
分配在栈上的Block和__block变量 其所属的变量作用域结束,该Block或者__block变量也会被废弃。
但是Blocks提供了 将Block和__block变量从栈上赋值到堆上的方法来解决这个问题, 这样即使语法记述其作用域结束,堆上的Block也能继续存在。
如图:
此时,赋值到堆上的Block将_NSConcreteMallocBlock类对象写入Block用结构体实例的成员变量isa;
impl.isa = _NSConcreteMallocBlock;
2、__block变量用结构体成员变量__forwarding存在的原因
上面的情况下只是Block,而__block变量需要用结构体成员变量__forwarding可以实现 无论__block变量配置在栈上还是堆上时都能够正确地访问__block变量。
在下面我们会说:在__block变量配置在堆上的状态下,也可以访问栈上的__block变量。 此时,只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,不管是从栈上的__block变量还是从堆上的__block变量都能正确访问。
Blocks提供的复制方法,如何从栈上复制到堆上的? ARC有效的时候,编译器可以自动判断。
来看Block函数:
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count){return rate*count;};
}
此时将会返回配置在栈上的Block的函数。 即 程序执行中,从该函数返回函数调用方时变量作用域结束,因此栈上的Block也被废弃。 虽然有问题,但是ARC的编译如下:
blk_t func(int rate) {
blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
tmp = objc_retainBlock(tmp);
return objc_autoreleaseReturnValue(tmp);
}
此时tmp为:blk_t __strong tmp。
这里的objc_retainBlock实际上是:_Block_copy
tmp = _Block_copy(tmp);
return objc_autoreleaseReturnVlaue(tmp);
将Block作为函数返回值返回时,编译器会自动生成复制到堆上的代码。
但是 当我们使用copy实例方法的时候,我们需要手动生成代码,将Block从栈上复制到堆上:alloc/new/copy/mutableCopy中的copy。
当我们向方法或者函数的参数中传递Block时 我们需要复制, 但是如果在方法或者函数中适当地复制了传递过来的参数,那么就不需要在调用该方法或函数前手动复制了。 如:
Cocoa框架的方法且方法名中含有usingBlock等。
Grand Central Dispatch的API。
例:
- (id) getBlockArray {
int val = 10;
return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0:%d", val);}, ^{NSLog("blk1:%d", val);}, nil];
}
id obj = getBlockArray();
typedef void (^blk_t)(void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk();
此时,blk()会发生错误,因为getBlockArray函数结束的时候,栈上的Block被废弃。 需要如下:
return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0:%d", val);} copy], [^{NSLog(@"blk1:%d", val);} copy], nil];
这里Block可以直接调用copy方法。
最后的blk = [blk copy];
对于配置在堆上的Block以及配置在程序的数据区域上的Block,调用copy会怎样 ?
如图:
我们多次调用copy又会怎样呢 ?
如:blk = [[[[blk copy] copy] copy] copy];
解释:
{
//配置在栈上的Block,赋值给变量blk中
blk_t tmp = [blk copy];
//将配置在堆上的Block赋值给变量tmp中,变量tmp持有强引用的Block
blk = tmp;
//将变量tmp 用Block赋值为变量blk, 变量blk持有强引用的Block。
//因为原先赋值的Block配置在栈上,所以不受此赋值的影响, 此时Block的持有者为变量blk和变量tmp
}
//由于Block被变量blk持有,所有没有被废弃。
{
//配置在堆上的Block被赋值变量blk,同时变量blk持有强引用的Block
blk_t tmp = [blk copy];
//配置在堆上的Block被赋值到变量tmp中,变量tmp持有强引用的Block
blk = tmp;
//由于变量blk进行了赋值,所以现在赋值的Block的强引用失效,Block被释放。
//由于Block被变量tmp所持有,所以没有被废弃。
//此时Block的持有者为变量blk和变量tmp
}
//由于变量blk还处于持有状态,Block没有被废弃。
{
blk_t tmp = [blk copy];
blk = tmp;
}
{
blk_t tmp = [blk copy];
blk = tmp;
}
__block变量存储区
现在来说一下使用__block变量的Block从栈上复制到堆上会有什么影响。
如图:
如果一个Block中使用__block变量,当该Block从栈赋值到堆时,使用的所有__block变量也必定配置在栈上。这些__block变量也全部被从栈复制到堆上。 此时Block持有__block变量。 即使在该Block已复制到堆的情况下,复制Block也对所使用的__block变量没有任何影响。
如图:
当多个Block使用__block变量呢 ? 此时,第一个block复制到堆上后,剩下的Block复制到堆上时,被复制的Block也会持有__block变量,并增加__block变量的引用计数。
如图:
如果Block被废弃,那么它所使用的__block变量被释放。
这里的思考方式与OC的引用计数式内存管理完全相同。
现在来看一下__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语法的表达式中使用初始化后的__block变量 堆上的__block变量
++val;
//Block语法之后使用与Block无关的变量。 复制前 栈上的__block变量
转换: ++(val.__forwarding->val);
在栈上的__block变量用结构体实例在__block变量从栈复制到堆上时,会将成员变量__forwarding的值替换为复制目标堆上的__block变量用结构体实例的地址:
这样,无论是在Block语法中,语法外使用__block变量,还是__block变量配置在栈或者堆上,都能顺利访问同一个__block变量。
截获对象
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = [^(id obj) {[array addObject:obj];NSLog(@"array count = %ld", [array count]);} copy];
}
blk ([[NSObject alloc] init]);
blk ([[NSObject alloc] init]);
blk ([[NSObject alloc] init]);
变量作用域结束,变量array被废弃,强引用失效。
但是代码运行正常, 这就说明赋值给变量array的NSMutableArray类的对象在该源代码最后Block的执行部分超出其变量作用域而存在。
转换后的源码:
//Block结构体,函数
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id __strong array;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong_array, int flags = 0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
id __strong array = __cself->array;
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
}
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
static void __main_block_desc_0 {
unsigned long reserved;
unsigned long 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
};
blk_t blk;
{
id __strong array = [[NSMutableArray alloc] init];
blk = &__main_block_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
blk = [blk copy];
}
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
这里要注意,被赋值NSMutableArray类对象并被截获的自动变量array,可以看到他是Block用的结构体中附有__strong修饰符的成员:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id __strong array;
};
之前说:oc中,c结构体中不能含有附有__strong修饰符的变量,因为编译器不知道何时进行c结构体的初始化和废弃。
但是oc的运行时库可以准确把握Block从栈复制到堆以及堆上的Block被废弃的时机。
所以 我们需要在__main_block_desc_0中增加copy和dispose,以及作为指针赋值给该成员变量的__main_block_copy_0函数和__main_block_dispose_0函数。
在源代码的Block中,含有附有__strong修饰符的对象类型变量array,所以需要恰当管理赋值给变量array的对象,因此__main_block_copy_0函数使用_Block_object_assign函数将对象类型对象赋值给Block用结构体的成员变量array中并持有对象。
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
_Block_object_assign 函数调用想让与retain实例方法。
当然在dispose函数中也是这样的。
_Block_object_dispose相当于 release实例方法。
我们来看看copy和dispose函数什么时候被调用:
什么时候栈上的Block赋值到堆?
1、调用Block的copy实例方法时。
2、Block作为函数返回值返回时。
3、将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时。
4、在方法名中含有usingBlock的Cocoa框架方法或Grand Central Dispatch的API中传递Block时。
在调用Block的copy的时候,如果Block配置在栈上,那么该Block会从栈复制到堆。 当Block作为函数返回值的时候,将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量的时候,编译器会自动将对象的Block作为参数,并调用_Block_copy函数。这与调用Block的copy实例方法效果相同。在方法中含有usingBlock的Cocoa框架方法或Crand
Central Dispatch的API中传递Block时,在该方法或函数内部对传递过来的Block调用Block的copy实例方法或者_Block_copy函数。
也就是说,虽然从源代码来看,在上面这些情况下栈上的Block被复制到了堆上, 但其实可归结为_Block_copy函数被调用时Block从栈复制到堆。
相反,释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose函数, 相当于对象的dealloc实例方法。
这样,通过使用附有__strong修饰符的自动变量,Block中截获的对象就能超出其变量作用域存在了。
截获对象和使用__block变量时的不同:
对象:BLOCK_FIELD_IS_OBJECT
__block变量:BLOCK_FIELD_IS_BYREF
通过标志来区分是对象还是__block变量。
但是与copy函数持有截获的对象,dispose函数释放截获的对象相同,copy函数持有所使用的__block比那里,dispose函数释放所使用的__block变量。
因此,Block中使用的赋值给附有__strong修饰符的自动变量的对象和复制到堆上的__block变量由于被堆上的Block所持有,因而可超出其变量作用域而存在。
在Block中使用对象类型自动变量时,除了以下3种情况,推荐调用Block的copy实例方法。
1、Block作为函数返回值返回时。
2、将Block赋值给类的附有__strong修饰符的id类型或Block类型成员变量时。
3、向方法名中含有usingBlock的Cocoa框架方法或Crand Central Dispatch的API中传递Block时。