【IOS学习】之九、Blocks的实现续

Blocks的存储域
Block和__block变量的实质就是 在栈上的结构体实例。   如:__block变量实质是 栈上__block变量的结构体实例。
其中Block也是oc的对象,该OC的类为:_NSConcreteStackBlock。 虽然该类并没有出现在源码,但是有很多与之类似的类:
_NSConcreteStackBlock     它的对象Block设置在栈上
_NSConcreteGlobalBlock    他与全局变量一样,设置在程序的数据区域(.data区)中。
_NSConcreteMallocBlock    它的对象设置在由malloc函数分配的内存块中(堆)

如图:
【IOS学习】之九、Blocks的实现续
【IOS学习】之九、Blocks的实现续

前面我们看到的是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也能继续存在。
如图:
【IOS学习】之九、Blocks的实现续
【IOS学习】之九、Blocks的实现续
【IOS学习】之九、Blocks的实现续
【IOS学习】之九、Blocks的实现续

此时,赋值到堆上的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
//将通过Block语法生成的Block, 即配置在栈上的Block用结构体实例, 赋值给相当于Block类型的变量tmp中
tmp = _Block_copy(tmp);
//_Block_copy函数   将栈上的Block复制到堆上,   复制后, 将堆上的地址作为指针赋值给变量tmp
return objc_autoreleaseReturnVlaue(tmp);
//将堆上的Block作为OC对象,  注册到autoreleasepool中,然后返回该对象。

将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会怎样 ?
如图:
【IOS学习】之九、Blocks的实现续
【IOS学习】之九、Blocks的实现续
我们多次调用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
}
//由于变量作用域结束,所以变量tmp废弃, 其强引用失效并释放所持有的Block
//由于Block被变量blk持有,所有没有被废弃。
{
//配置在堆上的Block被赋值变量blk,同时变量blk持有强引用的Block
     blk_t tmp = [blk copy];
//配置在堆上的Block被赋值到变量tmp中,变量tmp持有强引用的Block
     blk = tmp;
//由于变量blk进行了赋值,所以现在赋值的Block的强引用失效,Block被释放。
//由于Block被变量tmp所持有,所以没有被废弃。
//变量blk中赋值了变量tmp的Block, 变量blk持有强引用的Block。
//此时Block的持有者为变量blk和变量tmp
}
//由于变量作用域结束,变量tmp被废弃, 其强引用失效被释放所持有的Block
//由于变量blk还处于持有状态,Block没有被废弃。
{
     blk_t tmp = [blk copy];
     blk = tmp;
}
{
     blk_t tmp = [blk copy];
     blk = tmp;
}


__block变量存储区
现在来说一下使用__block变量的Block从栈上复制到堆上会有什么影响。
如图:
【IOS学习】之九、Blocks的实现续
【IOS学习】之九、Blocks的实现续
如果一个Block中使用__block变量,当该Block从栈赋值到堆时,使用的所有__block变量也必定配置在栈上。这些__block变量也全部被从栈复制到堆上。  此时Block持有__block变量。  即使在该Block已复制到堆的情况下,复制Block也对所使用的__block变量没有任何影响。
如图:
【IOS学习】之九、Blocks的实现续
【IOS学习】之九、Blocks的实现续
当多个Block使用__block变量呢 ?   此时,第一个block复制到堆上后,剩下的Block复制到堆上时,被复制的Block也会持有__block变量,并增加__block变量的引用计数。
如图:
【IOS学习】之九、Blocks的实现续
【IOS学习】之九、Blocks的实现续
如果Block被废弃,那么它所使用的__block变量被释放。
【IOS学习】之九、Blocks的实现续
【IOS学习】之九、Blocks的实现续
这里的思考方式与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变量用结构体实例的地址:
【IOS学习】之九、Blocks的实现续
【IOS学习】之九、Blocks的实现续
这样,无论是在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时。




-------2014/3/22 Beijing

【IOS学习】之九、Blocks的实现续,布布扣,bubuko.com

【IOS学习】之九、Blocks的实现续

上一篇:ios开发 加粗字体


下一篇:ios程序的启动过程