【Objective -- C】—— block
- Block简介
- 语法
- 初始化和声明
- Block类型变量
- typedef
- 截获自动变量
- __block说明符
- 截获的自动变量
- block的实现
- Block的存储域
- NSGlobalBlock
- NSStackBlock
- NSMallocBlock
- 深入理解Block的存储域
- Blocks如何实现复制到堆上
- __block变量的存储域
- 截获对象
- 调用时机
- __block变量和对象
- __block
- __weak
Block简介
block是一个带有自动变量值的匿名函数,它也是一个数据类型,跟int double float一样都是数据类型.所以我们是可以创建一个block类型的变量。
总结来说:block类似于一个方法。而每一个方法都是在被调用的时候从硬盘到内存,然后去执行,执行完就消失,所以,方法的内存不需要我们管理,也就是说,方法是在内存的栈区。 所以,block不像OC中的类对象(在堆区),他也是在栈区的。如果我们使用block作为一个对象的属性,我们会使用关键字copy修饰他,因为他在栈区,我们没办法控制他的消亡,当我们用copy修饰的时候,系统会把该 block的实现拷贝一份到堆区,这样我们对应的属性,就拥有的该block的所有权。就可以保证block代码块不会提前消亡。
语法
初始化和声明
返回值类型 (^block变量的名称)(参数列表)
void (^block1)()//无返回值,无参数
int (^block2)(int num1,int num2)//int类型返回值i,两个int类型参数
Block类型变量
Block类型变量与一般C语言函数变量完全相同,可作为以下用于使用:
- 自动变量
- 函数参数
- 静态全局变量
- 全局变量
使用Block语法,将Block赋值为Block变量。
int (^blk)(int) = ^(int count){return count + 1};
因为与通常的变量相同,所以当然也可以有Block类型变量赋值给Block类型变量。
int (^bilk1)(int) = blk;
- (void)blk3 {int (^blk)(int) = ^(int count){return count + 1;};int (^blk1)(int) = blk;int (^blk2)(int);blk2 = blk1;}
typedef
函数参数中使用Block类型变量可以向函数传递Block。但是在参数和函数返回值中使用Block类型变量极为复杂。这时,我们可以使用typedef来解决该问题。
typedef int (^blk_t)(int);
截获自动变量
blk的带有自动变量的匿名函数,什么是带有自动变量?
带有自动变量值在Block中表现为“截取自动变量值 ”。
NSString *c = @"ff";
NSString* (^addBlock)(NSString *a, NSString *b) = ^(NSString *a, NSString *b) {return [NSString stringWithFormat:@"%@%@%@", a, b, c];
};
__block说明符
自动变量值截获只能保存执行Block语法瞬间的值。
以下面的代码为例:
{int val = 10;const char* fmt = "val = %d\n";//自动变量值截获只能保存执行Block语法瞬间的变量的值。//执行瞬间fmt = "val = %d\n",val = 10;void (^blk)(void) = ^ {printf(fmt, val);};val = 2;fmt = "These values were changed. val = %d\n";blk();return 0;}
输出的结果为:
不允许块属性,仅允许在局部变量上 __block attribute not allowed, only allowed on local variables。
保存后就不能改写该值。若想在Block语法的表达式中将值赋给Block语法外声明的自动变量,需要在该自动变量上附加 __block说明符 。我们称这种变量为__block变量。
截获的自动变量
对于块来说截获Objective-C对象,调用变更该对象的方法不会产生编译错误。
- (void)blk4 {id tstArray = @[@"123", @"234", @"345"];id array = [[NSMutableArray alloc] init];void (^blk)(void) = ^{id obj = [[NSObject alloc] init];[array addObject:obj];};}
而向截获的变量array赋值则会产生编译错误。
- (void)blk4 {id tstArray = @[@"123", @"234", @"345"];id array = [[NSMutableArray alloc] init];void (^blk)(void) = ^{id obj = [[NSObject alloc] init];array = tstArray;};}
block的实现
Block的存储域
Block 和 ——block变量的实质
block既然是OC的对象,那么也存在许多的类似的类。
三种类对应了三种不同的区域:
NSGlobalBlock
如果一个 block 没有访问外部局部变量,或者访问的是全局变量,或者静态局部变量,此时的 block 就是一个全局 block ,并且数据存储在全局区。
//block1没有引用到局部变量int a = 10;void (^block1)(void) = ^{NSLog(@"hello world");};NSLog(@"block1:%@", block1);// block2中引入的是静态变量static int a1 = 20;void (^block2)(void) = ^{NSLog(@"hello - %d",a1);};NSLog(@"block2:%@", block2);
运行结果:
在Block内部不使用外部变量(不使用应截获的自动变量),或者只使用静态变量和全局变量。
NSStackBlock
int b = 10;NSLog(@"%@", ^{NSLog(@"hello - %d",b);});
运行结果:
可以发现,这边显示的不是NSStackBlock,原因是:在ARC环境下,系统会自动将block进行拷贝操作。只要改成MRC就行了。
与MallocBlock一样,可以在内部使用局部变量或者OC属性。但是不能赋值给强引用或者Copy修饰的变量
NSMallocBlock
什么时候栈上的 Block 会复制到堆呢?
- 调用Block的copy实例方法时
- Block作为函数返回值返回时
- 将Block 赋值给附有__strong修饰符id类型的类或Block类型成员变量时
- 在方法名中含有usingBlock的Cocoa框架方法或 Grand Central Dispatch的 API 中传递 Block 时
int a = 10;void (^block1)(void) = ^{NSLog(@"%d",a);};NSLog(@"block1:%@", [block1 copy]);__block int b = 10;void (^block2)(void) = ^{NSLog(@"%d",b);};NSLog(@"block2:%@", [block2 copy]);
运行结果:
那么三种储存方式有什么区别呢?
简单来说,没有捕获自动变量的就是数据区,捕获了自动变量但是没有进行copy操作就是栈区,copy之后就变成了堆区。
深入理解Block的存储域
为什么Block超出变量作用区域可以存在?
配置在全局变量上的 Block,从变量作用域外也可以通过指针安全地使用。 但放置在栈上的Block,如果其所属的变量作用城结束,该Blook 就被废弃。由于__block 交量也配置在栈上,同样地,如果其所属的变量作用域结束,则该_block 变量也会被废弃。
Blocks提供了把Block和__block复制到堆上的方法,防止block被废弃
复制到堆上的block将NSMallocBlock类对象写入Block用结构体实例的成员变量指针。
Blocks如何实现复制到堆上
对于OC来说,当ARC处于有效情况的时候,大多数情况下编译器会自己进行判断,自动生成把block从栈复制到堆上的代码。
以返回block的函数为例:
blk_t func(int rate) {return ^(int count) {return rate * count;};
};
- (void)testBlk {func(4);NSLog(@"%@", func(4));
}
该代码本应该是返回配置在栈上的block函数当程序结束的时候block应该被废弃,但实际打印出来的结果是mallocblock。
上面的代码翻译成源码为:
在ARC处于有效状态下,blk_t tmp 和blk_t strong tmp是一样的效果。
源码的 objc_retainBlock(tmp)其实就是Block_copy函数。
tmp = Block_copy(tmp)
return objc_autoReleaseReturnValue(tmp)
翻译源码的注释看实现栈到堆的过程发生了什么?
也就当Block作为函数的返回值返回的时候,编译器会自动生成复制到堆上的代码。
大多数情况下编译器会自主的进行判断拷贝,但是如下情况需要手动copy:
- 向方法或者函数里传递Block时。
但是在方法或者函数里面适当的复制了传递的参数时,那么就不需要手动复制。 - GCD的API不需要。
- cocoa框架方法且方法名包含usingBlock。
对于Block语法和Block类型变量都可以直接调用copy方法。
blkCopy = [blk copy];
对于配置在栈上的Block我们知道调用copy方法会讲Block从栈复制到堆上,那么存储在数据域和堆上Block调用copy会发生如下情况:
在不确定的情况下调用copy方法不会出现问题,即使是引用计数也有ARC帮我们自动管理。多次调用copy也不会出现问题。
int (^blk)(int) = ^(int count){return count + 1;};blk = [[[[blk copy] copy] copy] copy];
多次的copy就是一个持有,废弃,持有,废弃。。。的重复过程,对于ARC我们有自动管理,所以不会出现问题。
__block变量的存储域
__block变量的存储域和引用计数的模式类似,因为__block对象是在Block里被引用或者调用的,当Block被从栈复制到堆时,__block也有一样的变化。
在一个Block使用__block对象的时候,若Block从栈复制到堆,__block对象也从栈复制到堆,此时Block持有__block。
多个Block使用__block对象的时候,当一个Block从栈复制到堆,此时和一个Block情况一样,当其余Block从栈复制到堆的时候,Block会持有__block对象,此时__block对象引用计数加1;
当Block被废弃,那么它持有的__block对象也会被释放,这个是一一对应的关系,__block的思考方式和OC的引用计数内存管理方式完全相同。
通过Block的复制,可以同时访问堆和栈的__block变量。因为此时__block也从栈复制到堆,这与__block结构体使forward ing指针变量有关。
当栈上的__block被复制到堆上的时候,栈上的__block变量用结构体实例在__block变量从栈复制到堆上的时候,_forwarding成员变量的值从指向栈上的自身替换为目标堆上__block变量用结构体实例的的地址。
截获对象
在 ARC 中,捕获了外部变量的 block 的类会是 NSMallocBlock 或者 NSStackBlock,如果 block 被赋值给了某个变量,在这个过程中会执行 __Block__copy 将原有的 NSStakeBlock 变成 NSMallocBlock ;但是如果 block没有赋值给某个变量,那他的类型就是 NSStakeBlock ;没有捕获外部变量的 block 的类会是 NSGlobalBlock 既不在堆上,也不在栈上,它类似 C 语言函数一样会在代码段中。
在非 ARC 中,捕获了外部变量的 block 的类会是 NSStackBlock ,放在栈上,没有捕获外部变量的 block 时与 ARC 环境下情况相同。
书上展示了如下的代码:
typedef void (^blk_t1) (id);
// 截获对象
- (void)blkObj {id array = [[NSMutableArray alloc] init];blk_t1 blk;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]);NSLog(@"%@", blk);
}
代码首先生成并持有NSMutableArray对象,由于附有__strong目标变量的作用域立即结束,那么该对象应该被废弃,其强引用失效并释放,但是代码打印结果如图:
结果表示可变数组的类对象在Block的执行过程超出了变量的作用域而存在。
源码理解:
被赋值的自动变量array对象发现其在Block结构体中是带有__strong关键字的成员变量, 书上的解释是OC库能够很准确的把握从栈复制到堆,以及堆上被废弃的Block 因此即使结构体的成员变量含有__strong或者__weak关键字修饰的变量,编译器也可以适当的初始化和废弃。
Block源码里出现了 _main_block_copy_0 和 _main_block_depose_0 两个关键函数。
_main_block_copy_0
因为带有_-strong修饰符,所以 _main_block_copy_0函数调用 _Block_object_assgin函数来持有该array对象
_Block_object_assgin函数 相当于retain实例方法,将对象赋值在对象类型的结构体成员变量当中。
_main_block_depose_0
_main_block_depose_0函数调用 _Block_object_dispose,释放赋值在Block用结构体成员变量array中的对象。
_Block_object_dispose函数 相当于release实例方法,释放在对象类型的结构体成员变量中的对象。
调用时机
Block复制到堆上的情况:
- 调用 Block 的 copy 实例方法时。
- Block 作为函数返回值返回时。
- 将 Block 赋值给附有strong 修饰符 id 类型的类或 Block 类型成员变量时。
- 在方法名中含有usingBlock 的 Cocoa 框架方法或 Grand Central Dispatch 的 API 中传递
Block 时。
其表面看来是从栈栈复制到堆,本质是当调用到了_Block_copy函数的时候从栈复制到堆
相对的就是释放的时候,对于谁都不持有的对象会调用_Block_object_dispose函数,相当于dealloc方法,所以解释了上述截获对象array为什么可以超出其变量作用域而存在。
__block变量和对象
__block说明符可以指定任何类型的自动变量。
__block id obj = [[NSObject alloc] init];
该代码等同于:
__block id __strong obj1 = [[NSObject alloc] init];
ARC有效的时候 id类型以及对象类型变量必定附加所有权修饰符。
__block
该代码通过clang翻译源码如下:
上述出现了 _Block_object_assgin函数和 _Block_object_dispose函数。
**当在Block使用附有strong修饰符修饰的id类型或对象类型的自动变量时,**当Block从栈复制到堆的时候,使用_Block_object_assgin函数,持有被捕获对象,当堆上的Block被废弃的时候使用_Block_object_dispose函数,释放截获对象。
对于__block变量会发生相同的事情。所以即使对象赋值到堆上的附有strong修饰符的对象类型的__block变量中,只要__block变量在堆上继续存在,那么该对象就会继续处于被持有的状态。
__weak
对于之前的array代码,我们加入__weak修饰符试试.
这里的代码和书上的有所不同,按书上的我们需要重新初始化array释放,达到效果。
array = [[NSMutableArray alloc] init];
- (void)blkWeak {void(^blk)(id);id array = [[NSMutableArray alloc] init];id __weak weakArray = array;array = [[NSMutableArray alloc] init];blk = [^(id obj) {[weakArray addObject:obj];NSLog(@"array count = %ld", [weakArray count]);} copy];blk([[NSObject alloc] init]);blk([[NSObject alloc] init]);blk([[NSObject alloc] init]);NSLog(@"%@", blk);
}
同时加入__block试一试。
这是因为即使附加了__block说明符,附有__strong修饰符的变量array也会在该变量作用域结束的同时被释放被废弃,nil被赋值给附有__weak修饰符的变量的weakArray中。