内容大纲:
1、Blocks概要
2、Blocks模式
3、Block实质(面试常问重点)
1、Blocks概要
什么是Blocks:Blocks是C语言的扩充的功能,可以用一句话来表示Blocks的扩充功能:带有局部变量(有的资料局部变量也叫自动变量)的匿名函数。这个函数叫block。 (注意Blocks是一种功能,block是一种函数)
1-1、关于"匿名函数"
匿名函数:不带有名称的函数就是匿名函数。(但是C语言的标准不允许存在这样的函数。)
例如下面的源代码:在赋值给函数指针时,若不使用赋值的函数的名称,就无法取得该函数的地址:
然而通过Block,源代码中就能够使用匿名函数,即不带名称的函数。
1-2、关于"带有局部变量(有的资料讲局部变量也叫自动变量)"
这里为了能够让你们更好的理解这个"带有局部变量(有的资料局部变量也叫自动变量)",我需要拿普通的函数被回调的过程
和Block函数被回调的过程
进行对比,这样能够让读者更好的理解这个block的"带有局部变量(有的资料局部变量也叫自动变量)"的意义,以及这个block的特点。
(瞎扯两句:通过对比出与众不同的地方是可以成为特点的)
普通函数被回调的过程 和 block函数被回调的过程 的对比
为了能让读者进一步体会block的特性,本人分别在上面两个源码的函数被调用执行之前相应的地方添加了大括号设置局部变量的作用域,请看下面代码理解
总结:
补充:可能有读者觉得那block肯定能获取全局变量,可是全局变量普通函数也能获取啊,所以没必要在这里扯全局变量。(这句补充的话读者要是还不懂,那你就自行脑补吧。)
另 外介绍:"带有局部变量的匿名函数"这一概念并不仅指Blocks,它还存在于其他许多编程语言中,在计算机中,此概念也称为闭包(Closure)、 lambda计算(λ计算,lambda calculus)等,Objective-C的Block在其他程序语言中的名称如下表格:
2、Blocks模式
2-1、Block 语法
Block语法格式:^ 返回值类型 (参数列表) {表达式}
省略形式的语法(只有两种):
-
-
省略了返回值类型:^ (参数列表) {表达式}
-
省略了返回值类型和参数列表:^ {表达式}
-
2-2、Block 类型变量
先简单讲讲基本数据类型的类型变量,int a = 2
这个a就是int类型的变量,这个变量a存储着具体的值:2。
再讲讲函数指针类型变量:
这个funcptr就是函数指针类型的变量。这个变量指向(指针类型所以说是指向)func函数的地址。
那么同样的,在Block语法下,可将前面讲的"Block语法"赋值给声明为Block类型的变量中。
声明Block类型变量的格式:
返回值 (^变量名)(参数列表)
使用Block语法将Block赋值为Block类型变量:
关于Block类型变量声明部分的快熟记忆的方法:首先你肯定知道普通函数声明函数头部分:int func(),那么block的声明就是int (^func)(),其实就是在函数名的地方加了(^函数名),对比一下,是不是很好记了。
2-3 Block类型变量的使用
block类型变量,作为变量,它可以用在函数参数和返回值,但是这样的话,记述方式极为复杂。这时,我们可以像使用函数指针类型那样,使用typedef来解决这个问题:
2-4、截获局部变量值
关于截获局部变量值,其实在前面"1、Block概要"中其实已经介绍了,通过在适当的位置使用大括号,能够验证出Block会将局部变量拷贝一份为自己所拥有。
下面,通过另一种情况来验证,其实也很简单的,就是通过对外部的局部变量重新赋值。让我们再来看看这个"带有局部变量的匿名函数":
这个就是局部变量值的截获,截获之后被这个block所持有,因此叫做"带有局部变量的匿名函数"。
2-5、__block说明符
实际上,虽然block可以截获并拿到这个局部变量的值,但是却不能在block内部直接更改它,下面的代码会产生编译错误:
解决方法就是在这个m变量前面使用__block说明符
使用附有block说明符的自动变量可以在Block中赋值,该变量称为blcok变量。
2-6、截获的局部变量相关的问题
后期补充
3、Blocks的实现(面试常问重点)
3-1、Block的实质
Block是"带有局部变量的匿名函数",但是Block究竟是什么呢?本节将通过Block的实现进一步帮大家加深理解。
要想理解Block的实质,需要通过下面的终端命令将Block反编译成底层C++的源代码,虽然说是C++代码,其实也是仅仅使用了C++的struct结构,其本质还是C语言的源代码。
clang -rewrite-objc 源代码文件名
然后我们打开这个main.cpp文件,你会发现内容好多啊,多的你不要不要的:
没关系,很多代码都不是重点,重点的是和Block相关的代码,我们在main.cpp中可以先找到如下图的main函数,然后利用XCode颜色高亮插件DDHighlight,选择相关的关键字,同样关键字都会呈现出颜色,因为他们之间肯定存在调用和被调用的关系这样main函数中block低层实现的源码相关的东西,都可以找出,然后删除其他上百行无关的东西,也就剩下下面这30多行:
开始分析C++源码,如图是main函数中的block声明部分和block执行部分的代码和对应C++的源码:
我们先分析分析block声明的那段代码,block执行的部分先放一边,然后如图我做了进一步的处理,读者可以自己看图,我将声明结构体__block_impl内部的变量抽离出来,替换调用调用了结构体类型__block_impl声明impl的部分,以及处理了使用结构体变量impl的部分,不难,本质还是没变的:
按照上面意思我去掉不必要的注释,并且为了大家能够看得更清楚,我将其中长长的名字替换成简单的一目了然的名字,
比如__main_block_impl_0我全部替换成block_impl:
好,接下来就不得不多提一个C++的基础了,还好本人学过C++的基础部分敲过代码,上面一个代码的图中黄色框起来的部分是C++的结构体,在结构体blcok_impl中,多了一个看起来像C函数的函数,好像和我们习惯用的C语言结构体内部不带函数有区别,对于有一定Java基础或者是Swift基础或者是C++基础就会很熟悉这部分基础,如果不熟悉C++的结构体知识,那么读者可以阅读本人的《C语言的结构体和C++结构体的区别》。
也就是说,黄色框框的部分,结构体内部使用了构造函数或者是叫构造方法,我们通过初始化构造方法就能创建这个结构体的实例对象。
再看绿色框起来的代码,我把它拷贝过来:
block_impl((void *)block_func, &block_desc_DATA),
可以看得出,调用了block_impl这个结构体的构造方法,创建了这个block_impl这个结构体的实例对象,只不过,传入了两个参数:
1、(void *)block_func 2、&block_desc_DATA ,
而原构造函数需要三个参数:
1、void *fp 2、struct block_desc *desc 3、int flags=0,
很显然第三个参数已经被默认赋值为0,所以可以不需要传第三个参数。
关于block_desc_DATA这个我就不详说了,知道这么个东西就好,直接说说block_func,很明显,这个block的底层实现,就是创建了一个函数指针指向了一个函数,这个函数内部的代码逻辑就是我们Objective-C使用block所包含的代码逻辑。
下面,我们加进去之前为了避免干扰而删除的执行block部分的代码:
去掉多余的部分,我们可以看到:
本人虽然会一点C++基础,但是对于这黄色框起来的代码的写法本人也是第一次见过,所以不懂其基础细节,有大神懂得话,可以指教指教,
但是不管不懂还是不懂,不管你不懂还是我不懂,我们都可以大概的看的出来这黄色框起来的代码的意思就是
执行指针变量blk指向的函数FuncPtr
而回到前面 FuncPtr = void block_func(struct block_impl *__cself) { printf("Block\n"); }
就这样,完成了Objective-C的block的创建和执行。
3-2、截获局部变量值的block底层实质分析
根据上面简单的介绍,下面就不多累述了,直接截图出逻辑原理的代码图:
为了方便用"——>"画图,我将block_func和block_impl两个结构体调换位置。大家能理解就行。
总结一下,如果面试问到读者:请说说block的底层实现原理,读者可以这么回答:
"将block源码反编译成C++源码,可以看到,block底层通过使用C++的带有可以初始化成员变量的构造方法的结构体,来存储OC源码block截获的局部变量,并且这个结构体中还有一个成员函数指针,通过构造函数初始化可以指向另外声明的一个函数,而这个函数就包含了OC源码中block大括号括起来的代码逻辑,当我们执行这个block的时候,block底层C++源码就会取出这个结构体的成员函数指针变量,然后执行成员函数指针所指向的函数,这个函数包含两部分:1、在这个函数中取出了结构体的成员变量,这个成员变量存储了OC源码截获的局部变量,2、执行了所包含的OC源码中block大括号括起来的那部分代码逻辑"。