移动端开发 - iOS

iOS 对Block深入了解和应用

2019-08-16  本文已影响0人  背包技术

世事洞明皆学问,人情练达即文章。
腹有诗书气自华,人通国学身自重。

什么是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实例.png
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

  1. enumerateObjectsUsingBlock 这个是我最常使用的 block ,上面已经介绍过了,用来迭代数组非常方便,个人认为这应该是最好用的 block 了。
  2. enumerateObjectsAtIndexes:usingBlock: 和 enumerateObjectsUsingBlock 差不多,但是我们可以选择只迭代数组的一部分,而不是迭代整个数组。这个需要迭代的范围由 indexSet 参数传入。
  3. indexesOfObjectsPassingTest 返回一个数组中,通过了特定的 test 的对象的 indexSet。用这个block 来查找特定的数据很方便。

NSDictionary

  1. enumerateKeysAndObjectsUsingBlock 迭代整个字典,返回字典中所有的 key 和对应的值(如果是想用这个 block 来代替 objectForKey 方法,确实有些多此一举,但是如果你需要返回字典中全部的 key, value,这个 block 是一个很好的选择)。
  2. 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种

  1. 你可以引用 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内部直接修改该变量。

  1. 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

小结:此文持续更新和维护中,如中有不正确的地方,欢迎各路大神指导或在下方评论区讨论,不胜感激,共同成长。

部分资料来源于官方文档
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Blocks/Articles/bxGettingStarted.html#//apple_ref/doc/uid/TP40007502-CH7-SW1

上一篇下一篇

猜你喜欢

热点阅读