《iOS 三问》 -- Block (上) Block 是什么
对于一个 iOS 研发来说,Block
绝对是是日常使用率最高的几种工具之一。不知道大家在平常的使用之余有没有想过,Block 到底是什么?它是怎样设计出来的,底层是如何实现的?
从本章起,我们就来一起探究一下 iOS 中的 Block 技术。
Block 就是带有局部变量 (也叫自动变量) 的匿名函数。
上面这个也许是我们见过最多的 Block 定义了。老实说,可能你本来还大概知道 Block 是个什么东西,听到这个定义之后更懵逼了,局部变量、匿名函数,什么鬼?我们不妨抛开此定义,细想下,Block 为什么存在,是为了解决什么问题?
1 Block 是什么
其实,Block 精巧又单纯,其存在主要就是为了解决一个问题 –方法传递!
现在回想下我们平时使用 Block 的场合,比如说我们常用的 回调函数
其实就是传递一个方法、我们常用的 任务队列
其实就是一个个方法按序执行。在许多诸如此般的场合,我们需要将方法来传递:或将之作为方法的参数,或将之作为方法的返回值。
但是我们长期浸淫在面向对象编程的思想里,往往会建立这样的思维定式 – 我们所传递的都是对象,那么,方法是如何传递的呢? – 这样看起来简直就像是面向方法的编程~
主要有三种技术:
- 方法指针;
- 匿名内部类;
- 闭包。
1.1 方法指针、匿名内部类与闭包
1.1.1 方法指针
在 C 语言中,我们常使用 方法指针
技术来传递方法。使用 OC 编程时,我们可以随便地像下面使用方法指针,因为 Objective-C
是兼容 C 的:
1 | // 定义一个 C 语言方法 |
从上面的例子可见,使用方法指针可以使用方法的自由传递,也是比较灵活的。但是这样还是有点弊端:
- C 语言方法中不允许再定义方法。不能在方法内定义方法就导致我们想要的
匿名函数
这个 feature 无法实现。也就是说所有函数都要事先在外部定义好。 - 另外这种传统的方法定义功能上很局限。方法内不能包含
自动变量
,如果方法需要一些控制变量,则需要定义多个方法才能实现。
针对上面的第 2 点问题,我们展开一下说。C 语言方法毕竟是年代产物了,很多 “潮流的 feature” 它都不具备。我们说的 自动变量
的场合,比如,想像一下,我们有很多个回调函数 (比如网络访问回调),每个函数都需要不同的控制变量 (比如解密的 key)。如果用方法指针的方案,我们要定义无数个 responseWithKeyA ()
、responseWithKeyB ()
… 等方法。要解决这个 “控制变量” 的问题,人们想到了可以在外面包一层,我传递的不是直接的方法,而是一个结构体或对象,将方法和控制变量封装起来 –这就是 匿名内部类
的思想了
1.1.2 匿名内部类
有 Java 编程经验的同学对 匿名内部类
肯定不会陌生了,它主要就是我们上面所说的思路,将要传递的方法与控制变量封装成对象,调用时统一调用之前约定俗成的方法即可。比如我们熟悉的 Runnable
对象,我们把一个个任务封装成 Runnable
对象,执行方法放到 run
方法里面,然后可以通过自定义对象的属性达到自由定义控制变量的目的。
而 匿名内部类
其特点自然还有在 匿名
上。何为 匿名
?当然,字面上理解就是定义的结构是没有名字的,另外还有一层意思就是可以 嵌套定义
!你可以在类定义中再定义类,而不需要编写类定义那些繁琐的代码:
1 | // 定义一个 Task 任务类,当把任务对象加到任务队列上后,自动执行其中的 run 方法 |
这种思路是个好思路,但是还是有点缺陷:
- 一个是要写的多余代码太多,虽然
匿名内部类
已经做到了用最少代码定义一个类,但是仍然显得有点繁琐; - 另一个是
匿名内部类
的传参仍然不是很方便。
这时,第三种技术就运应而生了。
1.1.3 闭包
如今,稍微流行点的语言都会有 闭包
,函数式编程也渐渐成为潮流。
但是作为现实的程序员,我们不能只根据其流行程度决定一项技术是否好用。实际上 闭包
之所以流行,必是有其原因的。
- 其中之一就是简洁的语法,定义 Block 我相信大家其实都会了,下面我们介绍 Block 的应用时还会再罗列一下。
- 然后就是闭包的灵活性,在需要时,你可以直接在函数内定义一个闭包 (这就是之前聊过的所谓 “匿名” 的含义),你还可以将之自由地传递 (作为函数的参数或返回值)。
- 还有一个闭包最牛逼的特色,可以访问外层函数局部变量 (全局变量就不用说了)! 如下示例:
1 | //typedef 一个 Block 类型,名为 VoidBlock,无参数无返回值 |
上面的例子演示了 Block 作为闭包的闪亮特性:
1.嵌套定义,可在闭包内定义闭包;
2.闭包内访问外层函数局部变量,示例中的变量 a。
3.方法传递,将闭包作为返回值,这样我们的 myBlock 就像一个闭包的生成工厂,只要传入不同的参数就可以生成相应的闭包!!这种用法在 函数式编程
中称之为 柯里化
,有兴趣的童鞋可以自行了解下。
1.2 小结
现在我们了解了 Block 是什么,它其实是 OC 中关于闭包的一种实现,其主要是为了实现方法的传递及一些周边的特性。下面我们再介绍下 Block 的基本应用。
大家知道 Block 是闭包的实现之后,我们后面都不过份强调这个概念了,统一只称之为 Block。
2 Block 的应用
2.1 Block 的语法
2.1.1 Block 的定义
Block 的定义遵循 Block 定义范式。
1 | // -- Block 范式 -- |
2.1.2 Block 类型变量
和 C 语言的指针变量有点像,既然可以定义一个 Block,那必然得可以定义一个 Block 变量引用它,才可以实现传递和调用。
1 |
|
2.1.3 使用 typedef 简化 Block 类型的定义
像上面一样使用 Block 类型变量语法有点复杂,像 C 函数指针一样,Block 也可以使用 typedef
来简化定义。这也是我们工作中最最常见的使用 Block 的方法。
1 | // -- 声明 Block 类型变量语法 -- |
2.2 Block 内部访问外层数据
我们上面说到了闭包有个主要的特性,就是可以访问其外层函数的变量,这让往 Block 内传参变得非常方便。但是往 Block 传值的话又是要注意几点:
2.2.1 变量截取
- 关于
自动变量
与传值
首先我们要理清一个概念,Block 号称可以在内部访问外层变量,并将之称为 自动变量
。但实际上,这种访问并不是这么 自动
,其实也是通过传值来实现的。你可能会说,我并没有传值啊?不然,其实当你在 Block 中访问 Block 外部变量时,编译器已经为你生成了将此变量传到 Block 内部的代码了。或者这样说,我们之所以可以任意地访问 Block 外的变量,其实是编译器在背后默默地帮我们传值。
- 变量截取
那这个变量传值是怎么实现的呢?—- 这就是变量截取了。本章我们只讨论变量截取现象与应用,具体怎么实现的我们将在下章讨论。
1 | typedef int (^AddBlock)(int, int); // 定义一个 block 类型 |
从上面的例子我们可以发现,Block 可以访问外部的变量 inputValue,但是这个传入的 inputValue 就像是在 Block 定义的瞬间被 Block 截获了 – 也就是说,Block 中变量的为其瞬间值,就像是往 Block 中传了一个常量一样,是不可修改的。而且在定义 Block 的后继代码中再修改 inputValue 的值,对 Block 内的这个变量也不会有影响。
因此我们可以推想,在 Block 中访问外部对象的话也是一样,编译器可能帮我们自动做了个传值,将此变量当前值转成常量传了进来,以便在 Block 中访问。
当然,这里我们只要知道变量截取的表象就行。我们现在不防先这样设想着,下章我们将分析变量截取的原理,从中看看 Block 是如何实现访问外部变量或对象的。
2.2.2 __block 修饰符
上面我们说过,传入 Block 中的自由变量为截取变量,是不能修改的。但是这样会降低其灵活性,那么有没有方法能修改外部变量呢?– 答案是有滴~– 想修改传入 Block 块中的外部变量,需要在该变量上附加 __block
修饰符。
我们将上面的示例改造一下:
1 | AddBlock generateAddBlockWithExtra(int extra) |
大家对着 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 引用
- 《iOS-iOS 与 OS X 多线程和内存管理》 我学习本系列的主要参考书,写得非常好,真正的深入浅出。
- 《谈 Objective-C block 的实现 - 唐巧》 唐巧一篇早期的文章,写得不错的。
- 源码:libclosure-73,objc4-709
- 《iOS逆向-从汇编代码理解函数调用栈》 - 傻傻木,分析函数调用栈比较详细的一篇文章