1. Block的基本结构
|
|
将包含Block的代码通过clang转换为c++代码(只用了c++的扩展struct,实际上还是c)。我们一句一句看:
- Block变量的声明:
|
|
可以看到,testBlock变量,实际上是 __main_block_impl_0 结构体实例的指针。
__main_block_impl_0的结构为:
|
|
其中,用以识别Block对象的类型信息和Block的函数体都在 __block_impl 结构体中声明:
|
|
其中,FuncPtr指向的就是我们在Block中提供的函数体。而isa,即作为描述Block类型使用。由于Block在堆中也是遵循类似自动引用计数的内存管理机制,故可以把Block看做为对象。
而Block的描述信息,则是指向全局的 __main_block_desc_0 结构体的实例。
|
|
- Block的执行
|
|
了解了Block的结构,这一句就很好理解了。由于testBlock的地址与 __block_impl 指针的地址相同,因此直接转换为 __block_impl 类型。然后,获取其中的 FuncPtr 函数指针,传入自身作为参数后,直接调用执行。
传入自身作为 FuncPtr 的参数的目的:
由于Block的函数体在编译后成为了全局静态c函数(无状态保存)。因此,为了在调用时可以正常访问到捕获的变量,则将自身实例作为参数传入(这与OC调用方法的传参目的一样)。
2.Block捕获的变量
2.1 没有捕获变量
Block在没有捕获任何变量时,其类型(isa)为NSGlobalBlock。
2.2 捕获基本类型变量
测试代码:
|
|
在运行时,此Block的类型为NSMallocBlock。已经被copy到堆中。
对于基本数据类型的变量,捕获后,其值直接保存到 __main_block_impl_0 结构体中:
|
|
由于是值传递,直接修改此Block变量中的value是不会影响原value的值。因此,编译器则直接不允许修改捕获的变量。
而且,这也解释了为何在 FuncPtr 中的需要传入block自身作为参数:
|
|
2.3 捕获对象类型变量
测试代码:
|
|
在运行时,此Block的类型为NSMallocBlock。已经被copy到堆中。
由于捕获的是对象类型,因此编译后的c++代码与刚才有些不同:
|
|
核心结构还是一样,直接将捕获对象保存到了 __main_block_impl_0 结构体中。产生变化的,是 __main_block_desc_0 的结构:
|
|
由于捕获的变量是对象类型,因此,需要在结构体中指定实现内存管理方式的相应实现(clang可以在Block的相关结构体中对OC对象进行内存管理,但需要提供相应实现)。
|
|
也就是说,当Block变量被copy到堆上时,系统则会调用 _Block_object_assign 函数,对捕获的obj进行retain;而当堆上的Block变量被释放时,系统则会调用 _Block_object_dispose 函数,对捕获的obj进行release操作。
为了行为一致,编译器也不允许对捕获的对象类型变量进行修改。
这可以保证捕获的对象在超出自身作用域后,继续生存(因为已经被堆上的Block保留)。
2.4 捕获__block修饰的基本类型变量
测试代码:
|
|
首先,还是可以确认的是,在运行时,Block的类型是NSMallocBlock。
转换代码后,就可以看到,使用了 __block 修饰符的实现就变了很多。我们还是一句一句来看:
|
|
可以看到,__block 修饰的变量,实际上是一个全局的 __Block_byref_value_0 结构体的实例。我们看一下此结构体的内容:
|
|
可以看到,原始变量的真实值保存在结构体中。此结构体中不仅包含了类型标识、尺寸等信息,还包含了一个指向自身实例的指针。
下面是Block变量声明,只是将 __Block_byref_value_0 的地址传入,没有什么异常:
|
|
可以看到,唯一的区别就是,在捕获的带有 __block 修饰的变量,生成的Block变量中,是以引用传递的方式进行储存的。这也就意味着捕获的变量的内容是可以随意修改的,而且,访问或者修改的是 __Block_byref_value_0 的实例,而不是原始的变量。
对于Block中的描述信息,其实现也有些许变化:
|
|
可以看到,使用 __block 修饰的变量,在捕获到Block中后,也需要在Block被copy到堆上、或从堆中释放时提供对应的内存管理函数。
|
|
这里与捕获对象类型变量时,生成的内存管理函数中,区别只是类型不同,是 BLOCK_FIELD_IS_BYREF (捕获的对象类型变量是 BLOCK_FIELD_IS_OBJECT )。
与对象的保留关系不同,这种方式,实际上是创建一个新对象(结构体实例,如 __Block_byref_value_0 ,内部包含着被捕获的变量的值)直接存储在Block中。当Block被copy到堆上时,再创建一个新的 __Block_byref_value_0 实例,并保存在堆上的Block中。
在 __Block_byref_value_0 的结构中,为什么会包含一个指向自身实例的指针 __forwarding ?
为了保证访问到捕获变量的一致性。
在Block被copy到堆上时,不仅生成一个新的 __Block_byref_value_0 实例。而且将原始 __Block_byref_value_0 的 __forwarding 指针指向了新的实例。因此,通过形如 value.__forwarding->value 的方式,不管是在栈上,还是在堆上,都可以访问到堆中的同一个变量。
所以,我们最后看一下在Block执行之后,打印语句NSLog。
|
|
由于是在栈上执行,因此 value.__forwarding->value 最终指向的是堆上的Block中的新 __Block_byref_value_0 实例。
2.5 捕获__block修饰的对象类型变量
测试代码:
|
|
转换后的代码与 __block 修饰的基本类型变量很相似,都是生成一个对应的结构体实例,然后将变量存储在内部。
我们看一下生成过程(代码经过简化):
|
|
其中,__Block_byref_obj_0 的结构如下所示:
|
|
可以看到,__block 修饰的对象类型结构体,不仅包含与基本类型一样的成员,额外还包含了两个内存管理函数,用于在自身实例因Block的内存变化导致的变化时,包含的obj进行的保留和释放操作(Block的内存管理 -> __Block_byref_obj_0的内存变化 -> obj的内存变化)。
这里,我们看一下这一对内存管理函数的简单实现:
|
|
可以看到,以copy方法为例,实际上与描述信息 __main_block_desc_0_DATA 中的 __main_block_copy_0 函数实现一样,都是调用了 _Block_object_assign 函数。只不过参数有些许不同:
src+40偏移量即为 __Block_byref_obj_0 结构体中的obj的地址。131即 BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT 。
以 _Block_object_assign 实现为例(节选自苹果的Blocks源代码 Blocks/Sources/runtime.c):
|
|
可以看到,在这种情况下,copy操作只是使用一个新的指针指向原始obj。
在ARC下,实际上就是对obj进行了强引用,也就是retain操作;但是在非ARC下,这只是一个指针指向,可能造成悬垂指针访问,切记。
而在 __main_block_desc_0_DATA 中,使用的copy和dispose函数与 __block 修饰的基本类型变量一致:
|
|
最后,再看一下我们在Block函数体中对捕获变量的修改(代码已简化):
|
|
3. 总结
- 在ARC环境下,Block在不捕获变量时,是NSGlobalBlock类型;否则,都是NSMallocBlock类型。
- 捕获到的基本数据类型变量或OC对象,直接存储值到Block的数据结构中,为值传递,外部修改无效。
- 捕获到的__block修饰的基本类型变量或OC对象,是以包装成的新的结构体实例的方式存储到Block的数据结构中,为引用传递,可以进行修改。
- ARC环境下,Block从栈上到被copy到堆上时,捕获的OC对象或是block的OC对象,都会被retain;捕获的block的基本类型变量,会创建一个新的结构体,保存在copy后的Block中。
- 非ARC环境下,使用block修饰的OC对象,在被Block捕获后,可以防止循环引用(只是指针指向,没有retain操作,ARC下才是默认retain)。在ARC下,使用weak修饰变量替代。