这两天改bug改的太忙了....实际上该整理的知识点挺多的,今天就先说一下 Block 吧。

本篇文章中大量内容参考、转载自文章末尾的参考1,感谢原作者的总结与分享。原文中还有更多更详细的内容,欢迎大家点击原文学习更多内容。

什么是Block

让我们先简单的介绍一下Block

代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性,Block是一种特殊的数据类型,其可以正常定义变量作为参数作为返回值,特殊地,Block还可以保存一段代码,在需要的时候调用,目前Block已经广泛应用于iOS开发中,常用于GCD、动画、排序及各类回调。

综上我们可以得知:

  1. Block是一种数据类型。
  2. Block可以做变量,也可以做参数,还可以做为返回值,甚至用来保存一段代码。

Block变量

Block的声明

Block的声明格式为:返回值(^Block名称)(参数列表),让我们来举个栗子:

NSString *(^aBlock)(NSString *str, NSArray *arr);

如上是一个返回值NSString,名称为aBlock,拥有2个参数,分别为NSStringNSArray的一个Block代码块。

  • 注意:
    1. 其中形参的形参名,即栗子中的strarr可以省略不记。
    2. 这里的名称我们便可以将它想象成函数名

给Block变量赋值

我们声明完Block之后,就该给变量赋值了。

这种情况下的格式为:变量名 = ^返回值类型(参数类型){函数体}。举个栗子:

aBlock = ^NSString *((NSString *str, NSArray *arr) {
    NSLog(@"%@, %@", str, arr);
    
    return str;
};

这里实际上我们是把一个函数体赋值给了Block变量。从这里我们也可以看出来Block的一个重要用法,那就是用来存储函数体

  • 注意:
    1. 这里的返回值类型可以不用声明,直接省略。因为编译器可以从存储代码块的变量中确定返回值的类型。
    2. 如果没有参数列表,在赋值的时候可以省略。

声明的同时赋值

把上面两点综合起来就就可以了,这里直接给个栗子吧。

NSString *(^aBlock)(NSString *, NSArray *) = ^(NSString *str, NSArray *arr) {
    NSLog(@"%@, %@", str, arr);
    
    return str;
};

调用Block变量

Block变量的调用实际上和C语言函数的调用非常接近。为了节省时间,这里的参数str1arr我只进行了初始化,没有赋值。

NSString *str1 = [[NSString alloc] init];
NSArray *arr = [NSArray array];

NSString *str = aBlock(str1, arr);
NSLog(@"%@",aBlock(str1, arr));

是不是和C语言很像呢。

使用typedef定义Block类型

在实际使用Block的过程中,我们可能需要重复地声明多个相同返回值相同参数列表的Block变量,如果总是重复地编写一长串代码来声明变量会非常繁琐,所以我们可以使用typedef来定义Block类型。

// 定义一种无返回值无参数列表的Block类型
typedef void(^SayHello)();

// 我们可以像OC中声明变量一样使用Block类型SayHello来声明变量
SayHello hello = ^() {
    NSLog(@"hello");
};

// 调用后控制台输出"hello"
hello();

注意,在使用typedef的时候,(^)内的Block名已经被当做了整个Block类型的名称,而不单单是一个Block的名称。


在函数形参中使用Block

形参使用Block时函数的声明

Block作为函数形参是一个很重要的用法, 很多知名的第三方库或者网络请求中都会在函数的形参中用到Block。我们自己写代码的时候,偶尔为了方便高效,或者为了封装,也可以使用Block

其格式为:-(返回值)函数名称:(Block返回类型(^)(参数列表))函数参数名

  • 注意:
    1. 为了看起来清晰,在上面的格式中,我使用的都是中文标点,实际使用中肯定都要是英文标点的。
    2. 在作为形参使用时,将Block写在函数定义/声明的形参位置时,Block没有Block名称

下面是我自己的在一个项目中定义的一个函数,就使用到了Block作为形参。在这里大家理解这种声明的格式即可。

- (void)autoresizingMaskOfChildView:(NSMutableArray<NSNumber *> *(^)())autoMaskArr {
    NSMutableArray *tempArrM = [NSMutableArray arrayWithArray:autoMaskArr()];
}

上面定义了一个返回值为空void,名称为autoresizingMaskOfChildView,拥有一个Block参数的函数。

其中Block参数是一个返回值为存储NSNumberNSMutableArray,没有参数列表。

调用形参使用Block的函数

没有参数的Block形参

上面那个栗子中的函数,其调用的时候会是这样的:

[self.view autoresizingMaskOfChildView:^NSMutableArray<NSNumber *> *{
        <#code#>
    }];

我们在<#code#>中输入自己的代码,这一部分的代码相当于是赋值给了Block形参。

有参数的Block形参

我们先定义一个函数,方法中定义了一个block数据类型参数(返回值为int类型的,且带有一个int类型的形参)

- (void)calculate:(int (^)(int)) calculateBlock {
    //calculateBlock接受外界传入的代码块,也就意味着怎么去操作是由外界调用者决定的
    self.result = calculateBlock(_result);//将_result的值作为实参传入
}

外部控制器调用时如下所示:

[manager calculate:^int(int i) {
        //参数i自加1,然后返回
        i++;
        return i;
    }];
    NSLog(@"%d",manager.result);    //输出结果为1

通过最后的NSLog输出我们可以看到,在外界调用Block的时候,参数i的值被传给了函数定义时的参数_result。相当于_result = i

即是外界提供值,然后赋值给Block内部的变量。而不是从Block中将值传给外界。

  • 注意:上面这种写法实际上有很大问题,我们在这个函数声明的时候,没有说这个blcok是不能为空的,那么假如外界在调用这个block的时候,如果给block参数的位置传的是nil,而函数内部又给这个block传了参数,那么就会有产生Crach

例如下面这样:

[manager calculate:nil];

calculate:函数内部不变,依然是这样:

- (void)calculate:(int (^)(int)) calculateBlock {
    self.result = calculateBlock(_result);
}

这样就会导致Crach,所以我们在使用Block之前要对Block判空处理

- (void)calculate:(int (^)(int)) calculateBlock {
    if (!calculateBlock) {
        self.result = calculateBlock(_result);
    }
}

使用typedef简化Block

// 1.使用typedef定义Block类型
typedef int(^MyBlock)(int, int);

// 2.定义一个形参为Block的OC函数
- (void)useBlockForOC:(MyBlock)aBlock
{
    NSLog(@"result = %d", aBlock(300,200));
}

// 3.声明并赋值定义一个Block变量
MyBlock addBlock = ^(int x, int y){
    return x+y;
};

// 4.以Block作为函数参数,把Block像对象一样传递
[self useBlockForOC:addBlock];

// 将第3点和第4点合并一起,以内联定义的Block作为函数参数
[self useBlockForOC:^(int x, int y){
    return x+y;
}];

Objective-C 中的block属性

说完了形参与变量,那么在 Objective-C 的属性中该如何使用 block 语法呢?

定义 block 类型

首先我们应该定义一个 block 类型:

// 在#import 与 @interface 之间声明该 block 类型
typedef void(^ButtonEventsBlock)(UIButton *btn);

这样我们就定义了一个返回值为空,名称为 ButtonEventsBlock,带有一个 UIButton 类型参数block 类型了。

声明 block 属性

这一步很简单,就如同声明其他变量一样,使用之前定义好的 block 类型声明一个变量即可。

@property (nonatomic, copy) ButtonEventsBlock buttonEventsBlock;

对 block 属性赋值

这里我将演示两种方法:懒加载Runtime

懒加载

在代码中为 block 属性赋值和使用 懒加载block 赋值在语法层面没有太大的差异,所以这里就只演示懒加载了。

那么让我们直接砍代码吧:

- (ButtonEventsBlock)buttonEventsBlock {
    if (!_buttonEventsBlock) {
        _buttonEventsBlock = ^(UIButton *btn) {
            // your code
        };
    }
    return _buttonEventsBlock;
}

我们可以看到,在语法层面上,除了为 block 赋值的那部分外,其余部分与一般的懒加载并无差异。而为 block 赋值的那部分代码我们之前也已经提到过了。

Runtime

如果我们要在分类中使用 block 属性的话,我们就要借住 Runtime 来实现了。因为 Category 中并不会为我们自动生成成员变量

那么我们代码长什么样子呢?

static void *buttonEventsBlockKey = &buttonEventsBlockKey;

- (ButtonEventsBlock)buttonEventsBlock {
    return objc_getAssociatedObject(self, &buttonEventsBlockKey);
}

- (void)setMbc_buttonEventsBlock:(ButtonEventsBlock)buttonEventsBlock {
    objc_setAssociatedObject(self, &buttonEventsBlockKey, buttonEventsBlockKey, OBJC_ASSOCIATION_COPY);
}

哈,这里又和一般的在 Category 中使用属性的语法没有什么区别了。只不过,这里我们就没有办法使用懒加载为其赋值了,如果需要默认操作的话,我们只能在代码中对其赋值了。


参考

  1. 一篇文章看懂iOS代码块Block——蚊香酱
  2. iOS开发:Block作为参数使用(常见于各框架)——夏鲁鲁