《iOS 三问》 -- Block (上) Block 是什么

对于一个 iOS 研发来说,Block 绝对是是日常使用率最高的几种工具之一。不知道大家在平常的使用之余有没有想过,Block 到底是什么?它是怎样设计出来的,底层是如何实现的?

从本章起,我们就来一起探究一下 iOS 中的 Block 技术。

Block 就是带有局部变量 (也叫自动变量) 的匿名函数。

上面这个也许是我们见过最多的 Block 定义了。老实说,可能你本来还大概知道 Block 是个什么东西,听到这个定义之后更懵逼了,局部变量、匿名函数,什么鬼?我们不妨抛开此定义,细想下,Block 为什么存在,是为了解决什么问题?

1 Block 是什么

其实,Block 精巧又单纯,其存在主要就是为了解决一个问题 –方法传递

现在回想下我们平时使用 Block 的场合,比如说我们常用的 回调函数 其实就是传递一个方法、我们常用的 任务队列 其实就是一个个方法按序执行。在许多诸如此般的场合,我们需要将方法来传递:或将之作为方法的参数,或将之作为方法的返回值。

但是我们长期浸淫在面向对象编程的思想里,往往会建立这样的思维定式 – 我们所传递的都是对象,那么,方法是如何传递的呢? – 这样看起来简直就像是面向方法的编程~

主要有三种技术:

  1. 方法指针;
  2. 匿名内部类;
  3. 闭包。

1.1 方法指针、匿名内部类与闭包

1.1.1 方法指针

在 C 语言中,我们常使用 方法指针 技术来传递方法。使用 OC 编程时,我们可以随便地像下面使用方法指针,因为 Objective-C 是兼容 C 的:

方法指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义一个 C 语言方法
int myAddFunc(int a, int b)
{
return a + b;
}

// 定义一个方法指针
// 我们懂 Block 的可以看到,定义方法指针和定义 block 的语法很像:
// 定义 block 是: returnType (^blockName)(paramlist), 而方法指针则是 returnType (*ptrName)(paramlist)
// 只是把 * 号变成了 ^ 号。
int (*myMath)(int, int) = myAddFunc;

// 方法指针作为形参
void processTwoValue(int a, int b, int (*pf)(int,int))
{
printf("process(a, b) = %d", pf(a, b));
}

// 将我们定义的方法指针传入函数
processTwoValue(2, 1, myMath); // out ==> process(a, b) = 3

从上面的例子可见,使用方法指针可以使用方法的自由传递,也是比较灵活的。但是这样还是有点弊端:

  1. C 语言方法中不允许再定义方法。不能在方法内定义方法就导致我们想要的 匿名函数 这个 feature 无法实现。也就是说所有函数都要事先在外部定义好。
  2. 另外这种传统的方法定义功能上很局限。方法内不能包含 自动变量,如果方法需要一些控制变量,则需要定义多个方法才能实现。

针对上面的第 2 点问题,我们展开一下说。C 语言方法毕竟是年代产物了,很多 “潮流的 feature” 它都不具备。我们说的 自动变量 的场合,比如,想像一下,我们有很多个回调函数 (比如网络访问回调),每个函数都需要不同的控制变量 (比如解密的 key)。如果用方法指针的方案,我们要定义无数个 responseWithKeyA ()responseWithKeyB ()… 等方法。要解决这个 “控制变量” 的问题,人们想到了可以在外面包一层,我传递的不是直接的方法,而是一个结构体或对象,将方法和控制变量封装起来 –这就是 匿名内部类 的思想了

1.1.2 匿名内部类

有 Java 编程经验的同学对 匿名内部类 肯定不会陌生了,它主要就是我们上面所说的思路,将要传递的方法与控制变量封装成对象,调用时统一调用之前约定俗成的方法即可。比如我们熟悉的 Runnable 对象,我们把一个个任务封装成 Runnable 对象,执行方法放到 run 方法里面,然后可以通过自定义对象的属性达到自由定义控制变量的目的。

匿名内部类 其特点自然还有在 匿名 上。何为 匿名?当然,字面上理解就是定义的结构是没有名字的,另外还有一层意思就是可以 嵌套定义!你可以在类定义中再定义类,而不需要编写类定义那些繁琐的代码:

使用匿名内部类设置按钮回调
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个 Task 任务类,当把任务对象加到任务队列上后,自动执行其中的 run 方法
public class Task {
String key;
public void Task(Stirng key) { this.key = key; }
public void run() { /* process with key*/ }
}

// 把任务对象加到任务队列上后,自动执行其中 Task 的 run 方法。
// 我们可以在队列的 addTask 中定义匿名内部类,这些内部类继承自 Task,并重写其 run 方法。
// 同时可以操作传入的控制变量 key.
queue.addTask (new Task ("abc") { // 传入控制变量
run() { /* action with key */ }
});

这种思路是个好思路,但是还是有点缺陷:

  1. 一个是要写的多余代码太多,虽然 匿名内部类 已经做到了用最少代码定义一个类,但是仍然显得有点繁琐;
  2. 另一个是 匿名内部类 的传参仍然不是很方便。

这时,第三种技术就运应而生了。

1.1.3 闭包

如今,稍微流行点的语言都会有 闭包,函数式编程也渐渐成为潮流。

但是作为现实的程序员,我们不能只根据其流行程度决定一项技术是否好用。实际上 闭包 之所以流行,必是有其原因的。

  • 其中之一就是简洁的语法,定义 Block 我相信大家其实都会了,下面我们介绍 Block 的应用时还会再罗列一下。
  • 然后就是闭包的灵活性,在需要时,你可以直接在函数内定义一个闭包 (这就是之前聊过的所谓 “匿名” 的含义),你还可以将之自由地传递 (作为函数的参数或返回值)。
  • 还有一个闭包最牛逼的特色,可以访问外层函数局部变量 (全局变量就不用说了)! 如下示例:
Block 闭包示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//typedef 一个 Block 类型,名为 VoidBlock,无参数无返回值
typedef void (^VoidBlock)(void);

// 定义一个 Block 变量,此 Block 返回一个在内部定义的 Block
VoidBlock (^myBlock)(int) = ^(int a) {
NSLog(@"Define a block -> %d", a);
a++;

// 注意,这个内部 block 访问了外层函数变量 a
return ^(void) {
NSLog(@"Block in block, and access outer block's variable --> %d", a);
};
};

myBlock(1)();
// 这里的调用的一点点绕
// 首先调用 myBlock (1),返回其中的内部闭包 block,返回的这个 block 为 VoidBlock 类型,无参无返回值
// 然后直接调用返回的内部闭包
// output:
// > Define a block -> 1
// > Define block in block,and access outer block's variable --> 2

上面的例子演示了 Block 作为闭包的闪亮特性:
1.嵌套定义,可在闭包内定义闭包;
2.闭包内访问外层函数局部变量,示例中的变量 a。
3.方法传递,将闭包作为返回值,这样我们的 myBlock 就像一个闭包的生成工厂,只要传入不同的参数就可以生成相应的闭包!!这种用法在 函数式编程 中称之为 柯里化,有兴趣的童鞋可以自行了解下。

1.2 小结

现在我们了解了 Block 是什么,它其实是 OC 中关于闭包的一种实现,其主要是为了实现方法的传递及一些周边的特性。下面我们再介绍下 Block 的基本应用。

大家知道 Block 是闭包的实现之后,我们后面都不过份强调这个概念了,统一只称之为 Block。

2 Block 的应用

2.1 Block 的语法

2.1.1 Block 的定义

Block 的定义遵循 Block 定义范式。

Block 范式
1
2
3
4
5
6
7
8
9
10
11
12
13
//  -- Block 范式 --
// ┌───────────────────────────────┐
// │ ^ 返回值类型 参数列表 表达式 │
// └───────────────────────────────┘
// 其中:返回值类型可以省略,参数列表也可以省略

// 示例 - 求 2 数之和的 block
^int myAdd(int a, int b)
{
return (a + b);
}

myAdd (1, 2); //block 的调用就和普通 C 方法一样带上方法列表调用即可

2.1.2 Block 类型变量

和 C 语言的指针变量有点像,既然可以定义一个 Block,那必然得可以定义一个 Block 变量引用它,才可以实现传递和调用。

声明 Block 类型变量语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14

// -- 声明 Block 类型变量语法 --
// ┌───────────────────────────────┐
// │ 返回值类型 (^ 变量名) 参数列表 │
// └───────────────────────────────┘
//
// 示例:
// 声明并定义一个 myAdd block
int (^myAdd)(int, int) = ^ (int a, int b){
return (a + b);
};

// 调用刚定义的 block
myAdd(1, 2);

2.1.3 使用 typedef 简化 Block 类型的定义

像上面一样使用 Block 类型变量语法有点复杂,像 C 函数指针一样,Block 也可以使用 typedef 来简化定义。这也是我们工作中最最常见的使用 Block 的方法。

使用 ypedef 简化 Block 类型的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//  -- 声明 Block 类型变量语法 --
// ┌─────────────────────────────────────────┐
// │ typedef 返回值类型 (^Block 类型名) 参数列表 │
// └─────────────────────────────────────────┘
typedef int (^AddBlock)(int, int)

// 使用我们定义的 Block 类型,来定义一个 Block 变量
AddBlock myBlock = ^ (int a, int b){
return (a + b);
};

// 在日常工作中,我们常常会这样给一个类增加一个 Block 变量
@property (nonatomic, strong) AddBlock addBlock;

// 使用的时候直接设置 Block 方法体
[obj setAddBlock:^(int a, intb) {
return (a+b));
}];

2.2 Block 内部访问外层数据

我们上面说到了闭包有个主要的特性,就是可以访问其外层函数的变量,这让往 Block 内传参变得非常方便。但是往 Block 传值的话又是要注意几点:

2.2.1 变量截取

  • 关于 自动变量传值

首先我们要理清一个概念,Block 号称可以在内部访问外层变量,并将之称为 自动变量。但实际上,这种访问并不是这么 自动,其实也是通过传值来实现的。你可能会说,我并没有传值啊?不然,其实当你在 Block 中访问 Block 外部变量时,编译器已经为你生成了将此变量传到 Block 内部的代码了。或者这样说,我们之所以可以任意地访问 Block 外的变量,其实是编译器在背后默默地帮我们传值

  • 变量截取

那这个变量传值是怎么实现的呢?—- 这就是变量截取了。本章我们只讨论变量截取现象与应用,具体怎么实现的我们将在下章讨论。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef int (^AddBlock)(int, int); // 定义一个 block 类型

// 此 Block 工厂方法将生成并返回一个添加了特定参数的 Block
AddBlock generateAddBlockWithExtra(int extra)
{
int inputValue = extra;
AddBlock block = ^int (int a, int b) {
//inputValue += 1; 不可修改传入的 inputValue,会报错
return (a + b + inputValue); //***Block 内使用外部变量 inputValue***
};
inputValue += 10; //***变量截取,所以这里的改动不会影响 block 内变量的值***

return block;
}

AddBlock addBlock1 = generateAddBlockWithExtra(1);
AddBlock addBlock2 = generateAddBlockWithExtra(2);
addBlock1(1, 2); // output: 4
addBlock2(1, 2); // output: 5

从上面的例子我们可以发现,Block 可以访问外部的变量 inputValue,但是这个传入的 inputValue 就像是在 Block 定义的瞬间被 Block 截获了 – 也就是说,Block 中变量的为其瞬间值,就像是往 Block 中传了一个常量一样,是不可修改的。而且在定义 Block 的后继代码中再修改 inputValue 的值,对 Block 内的这个变量也不会有影响。

因此我们可以推想,在 Block 中访问外部对象的话也是一样,编译器可能帮我们自动做了个传值,将此变量当前值转成常量传了进来,以便在 Block 中访问。

当然,这里我们只要知道变量截取的表象就行。我们现在不防先这样设想着,下章我们将分析变量截取的原理,从中看看 Block 是如何实现访问外部变量或对象的。

2.2.2 __block 修饰符

上面我们说过,传入 Block 中的自由变量为截取变量,是不能修改的。但是这样会降低其灵活性,那么有没有方法能修改外部变量呢?– 答案是有滴~– 想修改传入 Block 块中的外部变量,需要在该变量上附加 __block 修饰符。

我们将上面的示例改造一下:

__block 修饰符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
AddBlock generateAddBlockWithExtra(int extra)
{
__block int inputValue = extra;
AddBlock block = ^int (int a, int b) {
inputValue += 1; //***在 Block 内可以修改__block 变量的值的***
return (a + b + inputValue);
};
inputValue += 10; //***加了 Block 之后,Block 外的修改也会反映到 Block 中***

return block;
}

AddBlock addBlock1 = generateAddBlockWithExtra(1);
AddBlock addBlock2 = generateAddBlockWithExtra(2);
addBlock1(1, 2); // output: 15
addBlock2(1, 2); // output: 16

大家对着 Block 调用结果 output 理解下,因为传入 Block 的 inputValue 变量指定了为__block 类型,所以 “变量截取” 特性消失,在 Block 中仿佛与外部是访问同一个变量,可以对之赋值,在外部的修改会影响 Block 内,在 Block 内的修改也会影响外部。

所以当传入的 extra 为 1 时, inputValue=1,然后 inputValue 在外部被 + 10 而变成 11。此时,addBlock 添加的额外增加变量 inputValue 是 11。当 Block 真正被调用时,此 inputValue 再自增 1 变成 12。因此我们调 addBlock1 (1, 2) 时,输出的值就是 12+1+2 = 15.

3 小结

通过本章的共同学习,现在我们了解了 Block 是什么,它其实是 OC 中关于闭包的一种实现,表现在 Block 上主要是 3 个特性:

1.可嵌套定义,可在函数及 Block 内嵌套定义 Block;
2.Block 内可访问外层变量,这里我们介绍了有 截取变量__block 变量 两种访问方式。
3.方法传递,block 的传递,可将 block 作为对象的属性、作为函数的入参或返回值。

然后我们还一起了解了下 Block 的基本应用,主要是 Block 的定义、使用 typedef 定义 Block 类型,最后我们还细细研究了 Block 是怎样访问外部变量的。如果我们不需要修改传入的参数,直接使用 截取变量 就行了。而如果我们需要对传入的参数进行修改,那么就要使用 __block 修饰符去声明一个 __block 变量

通过本章的学习我们对 Block 是什么就有比较全面的认识了,但是我们都知道,还有很多东西我们没有弄清楚,比如我们常说的 Block 循环引用是什么回事,为什么我们平时用 Block 时老是要用 weakify-strongify 技术,Block 源码是怎么实现的,截取变量和__block 变量又是怎样实现的。

接下来的几篇中,我们将对上述问题一一深入探讨。

4 引用

  1. 《iOS-iOS 与 OS X 多线程和内存管理》 我学习本系列的主要参考书,写得非常好,真正的深入浅出。
  2. 《谈 Objective-C block 的实现 - 唐巧》 唐巧一篇早期的文章,写得不错的。
  3. 源码:libclosure-73,objc4-709
  4. 《iOS逆向-从汇编代码理解函数调用栈》 - 傻傻木,分析函数调用栈比较详细的一篇文章