iOS 对Block深入了解和应用
世事洞明皆学问,人情练达即文章。
腹有诗书气自华,人通国学身自重。
什么是Block
Block 是C语言的扩充功能,在 iOS 4.0 中引入的新功能。
Block 能够让我们的代码变得更简单,能够减少代码量,降低对于 delegate 的依赖,还能够提高代码的可读性。
本质上来说,一个 Block 就是一段能够在将来被执行的代码。本身 Block 就是一个普通的 Objective-C 对象。正因为它是对象,Block 可以被作为参数传递,可以作为返回值从一个方法返回,可以用来给变量赋值。
在其他语言(Python, Ruby, Lisp etc.)中,Block 被叫做闭包——因为他们在被声明的时候的封装状态。Block 为指向它内部的局部变量创造了一个常量 copy。
在 Block 之前,如果我们想要调用一段代码,然后之后一段时间,让它给我们返回,我们一般会使用 delegate 或者 NSNotification。这样的写法没什么问题,但是使用过 delegate 和 NSNotification 大家就应该会感觉到——我们会不可避免的将代码写的到处都是,我们需要在某处开始一个任务,在另外一个地方来处理这个返回结果。使用 Block 就可以在一定程度上避免这个问题。
如何使用 Block
通过^操作符来声明一个快变量,块体本身包含在{}体中。
Block 的声明格式如下:
return_type(^block_name)(param_type,param_type,...)
^
符号其实就是专门用来表示:我们在声明一个 Block。
声明举例:
int (^myBlock)(int, int)
block 的定义格式如下:
^(param_type param_name, param_type param_name,...){
...
return return_value
}
要注意,定义和声明的格式有一些不同。定义以 ^ 开始,后面跟着参数(参数在这里一定要命名),顺序和类型一定要和声明中的顺序一样。定义时,返回值类型是 optional 的,我们可以在后面的代码中确定返回值类型。如果有多个返回 statement,他们也只能有一个返回值类型,或者把他们转成同一个类型。block 的定义举例:
^(int num1, int num2) {return num1 + num2};
我们把声明和定义放在一起:
int (^myBlock)(int, int) = ^(int num1, float num2){
return num1 + num2;
};
Block 调用:
如果将一个Block声明为一个变量,你就可以像使用函数一样使用它。
int resultFromBlock = myBolck(10, 20);
下面是一个完整的实例:
int multiplier = 7;
int (^myBolck)(int) = ^(int num){
return num * multiplier;
}
NSlog("%d",myBlock(3)); //result value 21
Block的应用解析
直接使用块,快是内联代码的集合。
1. enumerateObjectsUsingBlock
-(NSArray*)retrieveInventoryItems {
// 1 - 声明
NSMutableArray* inventory = [NSMutableArray new];
NSError* err = nil;
// 2 - 得到 inventory 数据
NSArray* jsonInventory = [NSJSONSerialization JSONObjectWithData:
[NSData dataWithContentsOfURL:[NSURL URLWithString:kInventoryAddress]]
options:kNilOptions
error:&err];
// 3 - 使用 block 遍历
[jsonInventory enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSDictionary* item = obj;
[inventory addObject:[[IODItem alloc]
initWithName:[item objectForKey:@"Name"]
andPrice:[[item objectForKey:@"Price"] floatValue]
andPictureFile:[item objectForKey:@"Image"]]];
}];
// 4 - 返回一个 inventory 的 copy
return [inventory copy];
}
我们在上面的代码中 3 处使用了 block:
使用了 enumerateObjectsUsingBlock 方法,把一个普通的 NSDictionary 转化成一个 IODItem 类的对象。我们对一个JSON Array 对象发送 enumerateObjectsUsingBlock 消息,迭代这个 array,得到 item 字典,然后用这个字典得到 IODItem,最后把这些对象添加到 inventory 数组然后返回。
2. enumerateObjectsUsingBlock内部模拟实现
你是否思考过它的内部实现原理,但由于objective-c不开源原因,我们只能来根据Block的原理来模拟它的实现,那么让我们来重写一个它的内部实现,通过NSArray的分类给外界提供一个测试方法,来模拟它的内部实现。
//分类.h文件
- (void)enumTestBlock:(void(^)(id obj,NSUInteger,BOOL *))block;
//分类.m文件
- (void)enumTestBlock:(void(^)(id obj,NSUInteger,BOOL *))enumBlock{
if (!enumBlock && self.count == 0) {
return;
}
BOOL stop = NO;
for (int index = 0; index < self.count; index ++) {
if (!stop) {
enumBlock(self[index],index,&stop);
}else{
break;
}
}
}
//外部调用
[@[@"1",@"2",@"3"] enumTestBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj isEqualToString:@"2"]) {
NSLog(@"index %lu",(unsigned long)idx);
*stop = YES;
}
}];
//index ---
这里唯一需要注意就是Block内传入的一个bool类型的指针参数BOOL *stop,是为了外部能在同一内存地址上修改内部实现中的变量stop的值,来实现跳出循环的功能。
总结一些比较常用的 block
NSArray
- enumerateObjectsUsingBlock 这个是我最常使用的 block ,上面已经介绍过了,用来迭代数组非常方便,个人认为这应该是最好用的 block 了。
- enumerateObjectsAtIndexes:usingBlock: 和 enumerateObjectsUsingBlock 差不多,但是我们可以选择只迭代数组的一部分,而不是迭代整个数组。这个需要迭代的范围由 indexSet 参数传入。
- indexesOfObjectsPassingTest 返回一个数组中,通过了特定的 test 的对象的 indexSet。用这个block 来查找特定的数据很方便。
NSDictionary
- enumerateKeysAndObjectsUsingBlock 迭代整个字典,返回字典中所有的 key 和对应的值(如果是想用这个 block 来代替 objectForKey 方法,确实有些多此一举,但是如果你需要返回字典中全部的 key, value,这个 block 是一个很好的选择)。
- keysOfEntriesPassingTest 和数组里的 indexesOfObjectsPassingTest block 类似,返回通过特定的 test 的一组对象的 key。
UIView Animation
animateWithDuration: animation: completion: 写过动画大家应该还是比较了解的,动画的 block 实现确实比非 block 实现简单、方便了很多。
GCD
dispatch async:这时异步 GCD 的主要功能,在这里其实最重要的是要理解 block 是一个普通的对象,是可以作为参数传递给 dispatch queue 的。
使用我们自己定义的Block
除了使用这些系统提供给我们的 block,我们有时候自己写的方法里也许也想要用到 block。我们来看一些简单的示例代码,这段代码声明了一个接收 block 作为参数的方法:
-(void)doMathWithBlock:(int (^)(int, int))mathBlock {
self.label.text = [NSString stringWithFormat:@"%d", mathBlock(3, 5)];
}
// 如何调用
-(IBAction)buttonTapped:(id)sender {
[self doMathWithBlock:^(int a, int b) {
return a + b;
}];
}
因为 block 就是一个 Objective-C 对象,所以我们可以把 block 存储在一个 property 中以便之后调用。这种方式在处理异步任务的时候特别有用,我们可以在一个异步任务完成之后存储一个 block,之后可以调用。下面是一段示例代码:
@property (strong) int (^mathBlock)(int, int);
// 存储 block 以便之后调用
-(void)doMathWithBlock:(int (^)(int, int))mathBlock {
self.mathBlock = mathBlock;
}
// 调用上面的方法,并传入一个 block
-(IBAction)buttonTapped:(id)sender {
[self doMathWithBlock:^(int a, int b) {
return a + b;
}];
}
// 结果
-(IBAction)button2Tapped:(id)sender {
self.label.text = [NSString stringWithFormat:@"%d", self.mathBlock(3, 5)];
}
还有,我们可以使用 typedef 来简化block 语法,当然效果和上面的是差不多的,我们来看下面的例子:
typedef int (^MathBlock)(int, int);
// 使用 tpyedef 声明一个property
@property (strong) MathBlock mathBlock;
-(void)doMathWithBlock:(MathBlock) mathBlock {
self.mathBlock = mathBlock;
}
-(IBAction)buttonTapped:(id)sender {
[self doMathWithBlock:^(int a, int b) {
return a + b;
}];
}
-(IBAction)button2Tapped:(id)sender {
self.label.text = [NSString stringWithFormat:@"%d", self.mathBlock(3, 5)];
}
我们将使用 block 与不使用 block 做一些对比
举例 :NSArray
普通 for 循环:
BOOL stop;
for (int i = 0 ; i < [theArray count] ; i++) {
NSLog(@"The object at index %d is %@",i,[theArray objectAtIndex:i]);
if (stop)
break;
}
这个 BOOL stop 现在看上去有点奇怪,但看到后面 block 实现就能理解了。
快速迭代
BOOL stop;
int idx = 0;
for (id obj in theArray) {
NSLog(@"The object at index %d is %@",idx,obj);
if (stop) break;
idx++;
}
使用 block :
[theArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){
NSLog(@"The object at index %d is %@",idx,obj);
}];
在上面的代码中, BOOL stop 设置为 YES 的时候,可以从block 内部停止下一步运行。
从上面三段代码的对比中,我们可以至少可以看出 block 两方面的优势:
- 简化了代码
- 提高了速度
举例:UIView Animation
非 Block 实现:
-(void)removeAnimationView:(id)sender {
[animatingView removeFromSuperview];
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[UIView beginAnimations:@"Example" context:nil];
[UIView setAnimationDuration:5.0];
[UIView setAnimationDidStopSelector:@selector(removeAnimationView)];
[animatingView setAlpha:0];
[animatingView setCenter:CGPointMake(animatingView.center.x+50.0,
animatingView.center.y+50.0)];
[UIView commitAnimations];
}
block 实现:
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[UIView animateWithDuration:5.0 animations:^{
[animatingView setAlpha:0];
[animatingView setCenter:CGPointMake(animatingView.center.x+50.0, animatingView.center.y+50.0)];
}completion:^(BOOL finished) {
[animatingView removeFromSuperview];
}];
}
同样我们可以看出 block 的优势:简化了代码,让代码保持在一起,不需要在一个地方开始动画,在另一个地方回调。读写起来都比较方便。苹果也建议这么做,不然苹果用 block 重写以前的代码干嘛呢~
最后我们来了解一下Block和变量之间的交互
在块对象的代码体中,变量可以用五种不同的方式处理即变量分为5种
- 你可以引用 3 种标准类型的变量,就像你在普通方法中使用的那样子:
(1)全局变量,包括static修饰过的静态变量,可读写。
(2)全局方法,技术上来说不能称为变量。
(3)局部变量和从上下文带进来的参数,仅可读。
block可以修改全局变量,是因为全局变量放在堆区,局部变量在栈区,所以不能修改,加上__block之后,相当于加了个标识位,遇到__block就把内存由栈区放在堆区。
Block不允许修改外部变量的值。Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。于是栈区变成了红灯区,堆区变成了绿灯区。
Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。
为什么从栈到堆就可以修改了呢?
Block默认的是NSGlobalBlock类似于函数,存放在代码段;当block内部使用了外部的变量时,block的存放位置变成了NSMallockBlock(堆),所以用__block修饰后才可以在block内部直接修改该变量。
- Block也支持另外两种类型的变量:
(4)函数级别上的 __block 修饰的对象。它在block里面是可以修改的,如果这个 block 被 copy 到了栈区,这个对象就会被强引用。
(5)const引入的。
最终,在一个方法的实现当中,blocks 也许会强引用 Objective-C 实例变量。
以下规则适用于在 block 中使用的变量
(1)可以接收全局变量,包括存在于上下文中的静态变量。
(2)传递到block中的变量(就像函数传递参数一样)
(3)相对于 block 块的非静态堆区对象被识别为 const 对象。他们的值会以指针的形式传递到 block 中。
(4)__block 修饰的对象允许在 block 中进行修改,并会被 block 强引用。
(5)在 block 块中实例化的对象,与在函数中实例化的对象基本一致。每一次调用这个 block 都会提供一个变量的 copy。相应的,这些对象可以被当做 const 或者是强引用的对象使用。
__block 存储类型(块存储类型)
您可以通过应用_block块存储类型修饰符来指定导入的变量是可变的(即读-写)。块存储与寄存器、自动存储和本地变量的静态存储类型相似,但相互排斥。
__block 变量在一个容器中存活,可以被变量的上下文共享,可以被所有 block 共享,可以被 copy 修饰过的 block 共享,以及在 block 块中创建的对象共享。也就是说,如果有任何 copy 出来的 block 用了这个变量,它会一直存活于堆区当中。
作为一个优化,block 存储开始与堆区,就像 blocks 他们自己做的那样子。如果这个 block 被 copy 了(或者在 OC 当中 block 接收到了 copy 消息)。变量就会被复制到栈区去。也就是说,这个变量可以一直被修改了。
对于 __block 变量有着两点限制:他们不能用于可变长度的数组,也不能包括C99中可变长度数组的结构体。
__block int x = 123; //x lives in block storage
void (^logXAndY)(int) = ^(int Y){
x = x + y;
NSlog(@"%d %d",x,y);
}
logXAndY(456);//x new is 579 y = 456
小结:此文持续更新和维护中,如中有不正确的地方,欢迎各路大神指导或在下方评论区讨论,不胜感激,共同成长。