《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逆向-从汇编代码理解函数调用栈》 - 傻傻木,分析函数调用栈比较详细的一篇文章
坚持原创技术分享,您的支持将鼓励我继续创作!