11、那么Block到底是怎么实现的呢?试一试通过将Block 的代码转换成普通C语言代码来查看它的实现过程。
要将OC代码转换成C语言代码,可以使用clang编译的一个命令:
通过这个命令能把指定文件中的OC代码改写成C++代码(其中主要部分还是普通的C语言代码),通过这些代码就能看到Block是如何使用C++语言实现的。
12、首先编写一个最简单的Block,没有返回值和参数列表,执行它会输出“Shayne Yeorg”。代码如下:
然后使用11的命令转换这个文件,可以得到文件main.cpp。main.cpp的内容很多,将其中相关的代码提取整理后如下:
这便是这个Block转换后的主要内容了。
13、第一次看到这段代码会觉得有点乱,毕竟使用的变量名虽然有规律但是比较长,为了更好地理解这段代码,试着将主要的命名替换成伪代码,如下:
这时候这段代码的内容就很清晰了:
(1)、它首先声明了一个struct来存放Block对象的基础结构(下文记为struct1),这个struct还不是完整的Block,只是组成部分,它主要包含了一个isa指针和一个函数指针FuncPtr;
(2)、然后声明了一个带有编号的具体函数(匿名函数),可以注意到函数里的内容其实就是源码中Block中要执行的内容了;
(3)、再声明一个struct用来存放Block对象的size等其他相关信息(下文记为struct2),同时定义一个此结构体的具体实例;
(4)、有了以上的准备后,就可以声明一个完整的Block对象的struct了,它由上面的struct1和struct2组成。
除此之外,还对这个完整的Block对象的struct声明了一个构造函数,使用这个构造函数不仅可以为其内部的struct2赋值,还能为struct1的函数成员FuncPtr赋值;
(5)、最后就是main函数了,在main函数内,首先调用完整Block对象的struct的构造函数定义了一个Block 对象,构造函数使用的参数是(2)的函数和(3)中声明的struct2的实例,达到了源码中定义Block的效果。
生成这个Block对象后,下一步就是调用这个Block对象内部的struct1的函数成员FuncPtr,达到了源码中调用这个Block的效果。
(6)、这就是一个最简单的Block对象的完整实现过程。
14、从13的实现过程可以分析出:
(1)、在定义FuncPtr函数的时候,可以注意到它是有参数的,参数是一个Block完整struct类型的变量__cself。
这类似于消息发送中,通过方法调用表找到对应方法的实现后,传递给方法实现的两个隐藏参数self和_cmd,在方法实现中通过self和_cmd就可以获取到objc_messageSend()函数的两个必要参数。
类似的,在这里,函数中通过__cself就可以访问到调用这个函数的Block本身了。
这个参数在13的例子中还没有用到,那只是一个最简单的Block,下文会涉及到使用这个参数的情况;
(2)、Block内部是有一个isa指针的,说明了它也是对象。并且在构造这个Block的时候,这个指针被指向了_NSConcreteStackBlock,关于这个将在下文详解;
(3)、Block的回调功能,本质上是通过调用函数实现的。
15、通过13的简单Block例子明白了Block的3个主要功能点之一的回调功能的实质,那么接下来来研究一下另一个功能点:截获变量。看一看有截获变量的Block 在转换为普通C语言后是什么样的。编写以下代码:
这段代码在定义Block之前先定义了两个变量,并且在Block中使用了这两个变量。执行这个Block 会打印出“Shayne Yeorg's shirt number is 10”。
16、使用clang命令重写代码,得到的C++代码中提取出的主要部分如下:
其中__block_impl结构的声明部分省略了,这部分是不会变的。
17、分析16的代码:
(1)、在Block的完整结构体__main_block_impl_0中,多了两个成员,成员的类型和命名和截获的变量name和shirtNum一样,并且它的构造函数也增加了对这两个成员变量的初始化;
(2)、这时__main_block_func_0函数就使用到__cself了,它通过__cself能获取到对应的__main_block_impl_0对象,然后就能取得两个成员变量name和shirtNum的值来使用了;
(3)、在main函数中,构造Block对象的时候,把定义的name和shirtNum作为参数传递进去了,所以在下一句代码调用funcPtr的时候,就能通过(2)的方法取得这两个变量的值了;
(4)、所以,Block截获变量的处理方法其实就是在其结构体内增加和截获变量相同类型的成员变量,然后将截获变量的值保存在成员变量里。这也就解释了,为什么一个变量被Block截获后还去修改它的值,也不会影响到Block中已截获到的值。
18、明白了Block的回调和截获变量的功能的实现方法后,再来看一下Block的最后一个功能:截获可变变量。
在Block里可以修改的变量,在声明的时候需要附上__block修饰符,这种变量和普通的变量是有很大区别的,首先来看一看在没有涉及到Block的情况下,__block变量的实质。编写以下代码来查看区别:
使用clang命令转换这段代码后,提取出主要部分如下:
19、在18中,可以注意到:
(1)、附上__block的变量,被转换成了一个全局的结构体,结构体中不仅包含了变量的值blockVar,还包含了一个__forwarding指针,从初始化的代码来看,这个指针指向了blockVar结构体自身;
(2)、修改blockVar变量的值的时候,并不是直接修改blockVar结构体中的blockVar成员的值,而是通过结构体中的__forwarding指针重新定位到blockVar结构体(在这里其实就是它自己),然后修改找到的这个blockVar结构体里的blockVar成员的值。
20、为什么修改修饰为__block的变量的值的时候不直接修改,要使用__forwarding指针绕了一圈回来修改呢?
这是因为在某些情况下(具体情况后文详解),截获了__block变量的Block会被从栈上复制到堆上,同时__block变量也会被复制一份到堆上,这时留在栈上的__block变量的__forwarding指针就会被置为指向堆上的__block变量。
这个时候,如果在Block之外使用这个__block变量,虽然使用的却仍是存放在栈上的变量,但是通过__forwarding指针却能顺利访问到堆上的__block变量,也就实现了Block内外访问的都是同一个__block变量的效果。
21、明白了__block变量的实质后,来看一看截获了可变变量的Block究竟是怎么实现的。编写以下代码:
(图J截获可变变量的Block1)
这段代码在Block中修改了__block变量的值,将这段代码进行转换:
22、分析21转换后的代码:
(1)、和18一样,__block变量shirtNum被转换成了一个全局结构体__Block_byref_shirtNum_0;
(2)、类似16的截获不可变变量的代码的处理方式,Block的完整结构体__main_block_impl_0也会增加一个__Block_byref_shirtNum_0结构体类型的成员shirtNum,通过构造函数可以看到它会被赋值为一个__Block_byref_shirtNum_0结构体的__forwarding成员指针;
(3)、在__main_block_func_0函数中,在通过__cself参数获取到__main_block_impl_0对象的shirtNum成员变量的值后,是通过shirtNum->__forwarding->shirtNum这样的方式去修改它的值的;
(4)、增加了__main_block_copy_0函数和__main_block_dispose_0函数,这两个函数的作用类似于NSObject的retain和release方法,只不过它们是用来管理Block对象的,用于Block对象从栈上复制到堆上以及从堆上废弃的时候;
(5)、在main函数中,通过定义__Block_byref_shirtNum_0结构体的对象,来达到定义__block变量的效果,这点也和18一样;
(6)、这就是截获了可变变量的Block的实质。