一 Block的实现
1. 在main函数中声明、实现并调用一个block
2. 然后我们通过clang命令将main.m转为c++文件
clang命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
3. block的声明和实现
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
在block的实现中使用了__main_block_impl_0函数,并将__main_block_impl_0函数返回值的地址赋值给了我们声明的block变量“block”
4. __main_block_impl_0
在__main_block_impl_0的结构中我们看到一个同名的构造函数(上图红框标记的函数)。这里我们可以看出,实现block时,调用__main_block_impl_0函数,并返回了一个__main_block_impl_0结构体,并取了此结构体的地址,所以变量“block”的实质是一个__main_block_impl_0结构体的地址。
5. __block_impl
可以看出__block_impl结构与oc对象一样,都有一个isa指针。其中FuncPtr字段保存的就是自定义逻辑代码块转化后函数的指针。
5 __main_block_impl_0构造函数的调用
此构造函数有三个参数,其中第三个参数在构造函数声明时直接赋值为0(int flags=0),所以我们不用考虑第三个参数,主要分析另外两个参数,__main_block_func_0和&__main_block_desc_0_DATA
6. 参数__main_block_func_0
这里有个NSLog,我们分析这里就是我们实现block时的具体逻辑。我们在实现block时,将自定义逻辑的代码块封装成了__main_block_func_0函数,并将__main_block_func_0函数的地址作为参数传入了__main_block_impl_0构造函数,将__main_block_func_0存储在__main_block_impl_0结构体中。
7. 参数&__main_block_desc_0_DATA
在上图中我们看到__main_block_desc_0_DATA是对结构体__main_block_desc_0初始化后的结果。在初始化时reserved被赋值为0,Block_size中存储着__main_block_impl_0结构体占用空间的大小。
8. __main_block_impl_0构造函数的实现
我们可以看到:
impl.isa存储着表示block类型的内容,这表明block实质上是一个OC对象。
impl.Flags中存储着0,是源码中写死的,暂时不讨论此参数。
impl.FuncPtr中存储的是block自定义逻辑代码块封装的函数地址。
Desc中存储着整个__main_block_impl_0结构体所占空间的大小。
9. 一张图总结一下block的实现过程
二 Block的调用
我们知道实现block时,自定义逻辑的代码块是存储在了FuncPtr中,所以调用过程实际就是找到FuncPtr并调用。
我们从上面__main_block_impl_0结构体内存示意图中看出,__main_block_impl_0结构体的第一个成员是impl,它的地址与__main_block_impl_0的地址其实是一样的,所以我们直接把__main_block_impl_0结构体强转为__block_impl结构体,然后直接调用其中的FuncPtr。
这样就完成了block的调用。
三 Block的变量捕获
1. 捕获局部自动变量
上图中变量a是一个局部自动变量(省略了auto修饰词)。从图中我们可以分析出,block中访问block外的局部值类型参数时,是通过__main_block_impl_0结构体缓存住参数值,然后再传入__main_block_func_0函数中的。
这是一个简单的变量捕获过程。
2. 捕获多种值类型变量
这里声明了4个变量:a,局部自动变量;b,局部静态变量;c,全局变量;d,全局静态变量。
这四个变量被block捕获的逻辑如下:
a:通过__main_block_impl_0结构体缓存住a的值,然后通过__main_block_impl_0结构体获取到a的值
b:通过__main_block_impl_0结构体缓存住b的地址,然后通过__main_block_impl_0结构体获取到b的地址,最终获取到b的值
c:直接访问
d:直接访问
这样我们就能解释下图这种情况了
我们在实现block之后修改了变量a、b、c、d的值,在打印时发现,a的值并没有改变。这是因为在实现block的值被赋值给了__main_block_impl_0结构体内的成员,这时block中使用的a已经不是block外声明的那个a了。
四 Block的类型
1. block的类型
图中定义了6种情况的block:
1. 没有使用外部变量 2. 使用了局部变量 3. 使用了静态局部变量
4. 使用了全局变量 5. 使用了全局静态变量 6. 直接调用
结果可总结为:
这里我们看到block有三种类型,分别为NSGlobalBlock、NSMallocBlock和NSStackBlock。
2. block类型间的关系与区别
从上面6个block的分析中我们可以看出当block中没用通过__main_block_impl_0结构体直接存储变量的值时,block的类型为NSGlobalBlock。
当block中通过__main_block_impl_0结构体重存储了变量的值时,block的类型为NSStackBlock。
但是这里有一个特例,block1也在__main_block_impl_0结构体重存储了变量的值,但是它的类型却是NSMallocBlock。
这是因为在ARC模式下,如果有变量引用了block,则block会自动被copy一次,将block从栈区拷贝到堆区。此时block的内存控制由引用计数控制。在MRC过程中是没有copy操作的。
由此我们可以知道,我们在ARC环境中生命block属性时,使用copy和strong关键词修饰最终的效果都是一样的,用strong修饰时block也会自动copy。我们现在还是用copy修饰block属性主要是延用MRC时代的写法。
NSGlobalBlock:存储在数据段(全局区),内存在程序结束后由系统回收。
NSStackBlock:内存由编译器控制,过了作用域就会被回收。
NSMallocBlock:内存根据引用计数控制。
3. block clang后的类型
我们可以看到,block0~block5对应着__main_block_func_0~__main_block_func_5这个五个结构体,下面我们分别看下这五个结构体中的isa指针指向什么。
clang之后的block类型全部都是_NSConcreteStackBlock类型,与我们之前打印的结果不一致。我在网上找到了一些其他大神的分析,记录在这里。
1. block0,block2,block3,block4并未使用外部变量,应该是一个_NSConcreteGlobalBlock,而用clang显示的却是_NSConcreteStackBlock,唐巧的说法是:clang 改写的具体实现方式和 LLVM 不太一样。
2. 把编译后的代码copy到mm中后
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();
此行会crash,因为((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))指向的是impl的isa,也就是_NSConcreteStackBlock的指针。
需要改写成
struct __main_block_impl_0 impl_0 = __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
((void (*)())(impl_0.impl.FuncPtr))();
才能执行成功。
或许clang改写的实现跟LLVM确实不一样吧。
作者:WhiteZero链接://www.greatytc.com/p/d96d27819679来源:简书著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
4. block类型的具体实现
我们在block捕获局部自动变量a时发现,clang后的block实现自动添加了一个变量a,这个a是从哪里来的?我们需要进一步探寻block类型的底层结构。
源码简要分析
我们在源码中找到Block_private.h文件中找到Block_layout结构体,此结构体就是block的真实结构。
最后面有个注释,表示输入的变量也会保存在此结构体中,旁边的内存示意图中variables就是保存输入的变量的空间。
其中的isa就是指向block类型的(_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock)
五 Block捕获对象类型参数
1. 源码分析
搞清楚了block的源码结构之后,我们再来看看NSMallocBlock类型的block是怎么通过copy操作放进堆区的。
1. Block_copy()
我们现在知道NSMallocBlock类型的block是对NSStackBlock类型的block进行了一次copy后得到的,我们现在要来研究一下这个copy方法中做了什么操作。
我们在block源码中的Block.h中看到了这个copy方法的定义,叫做Block_copy(...)
其本质是_Block_copy()方法,这个方法的具体实现在runtime.cpp文件中
如图,objc的引用计数为3,这是因为,new出来的内存区域在赋值给objc时引用计数加一,在创建stack类型block时,捕获外部变量过程中会拷贝一份,这时引用计数加一,在将block赋值给变量“block”时又执行了一次copy操作,这时objc的引用计数又加了一,所以此时objc的引用计数是3。
3. Block_descriptor_2
我们从上边_Block_copy()方法分析中看到,在copy过程中调用了_Block_call_copy_helper函数,在这个函数中判断了是否有Block_descriptor_2结构体,如果有的话我们将调用其中的copy函数。
我们先来看下Block_descriptor_2结构体是什么样的。
其中copy和dispose都是一个函数的指针,而这两个函数具体在哪里实现的呢?
我们构造一个block,不能block中捕获一个OC对象的变量,clang之后就可以看到这两个函数。
我们在__main_block_desc_0_DATA结构体中看到,多了两个字段,而且在初始化时将__main_block_copy_0和__main_block_dispose_0这两个函数的地址传了进去。
我们接下来分析这两个函数。
4. __main_block_copy_0
__main_block_copy_0函数的本质是_Block_object_assign函数,我们分析一下_Block_object_assign函数。
5. __main_block_dispose_0
__main_block_copy_0函数的本质是_Block_object_dispose函数,我们分析一下_Block_object_dispose()函数。
6. 总结一下
(1)在实现block时,如果block引用了外部局部变量,且block被引用时,block的类型被设置为NSMallocBlock类型
(2)对象的传递与值类型一样也是在__main_block_impl_0结构体中新增一个字段保存
(2)在block中捕获OC对象的参数后看到,系统自动构造出了__main_block_copy_0和__main_block_dispose_0两个函数
(3)__main_block_copy_0和__main_block_dispose_0这两个函数保存在__main_block_desc_0_DATA结构体中
(4)NSMallocBlock是NSStackBlock copy得来的,在copy的过程中通过__main_block_copy_0和__main_block_dispose_0这两个方法对捕获的对象进行内存管理。
六 __block修饰符
1. __block修饰符源码分析
当我们希望在block中修改变量的影响范围扩展到block之外,我们需要在声明变量时使用__block修饰符,那这个修饰符具体是怎么样实现这一功能的?我们先clang一下,看下具体实现是怎样的。
可以看到,用__block修饰的变量被构造成了__Block_byref_objc_0类型的结构体,在构造函数__main_block_impl_0初始化时,对象objc被赋值为__Block_byref_objc_0结构体的__forwarding字段。
我们先来看下__Block_byref_objc_0结构体的结构。
这里看到前面赋值给objc字段的__forwarding也是一个__Block_byref_objc_0结构体,还是看不出来__block是如何实现的。
2. __block修饰符原理
我们先将main.h文件修改到mrc模式,修改方式如下
修改到mrc模式之后实现下面的代码
打印结果如下
在copy的过程中,有__block修饰的OC对象,会被构造在__Block_byref_object_0结构体的__forwarding字段中。
在栈block中,__forwarding指向的是自己(栈中的__Block_byref_object_0结构体),栈block经历了copy操作之后,将__Block_byref_object_0结构体拷贝到了堆中。
堆中__Block_byref_object_0结构体的__forwarding指向的也是自己(堆中的__Block_byref_object_0结构体),
但是此时栈中__Block_byref_object_0结构体的__forwarding指向的不是自己了(指向了堆中的__Block_byref_object_0结构体)。
我们看看此时block中使用objc的具体实现
在使用objc时通过__forwarding重新指向了对象的存储位置,这样copy之后无论是在栈block中还是堆block中访问的都是同一个objc对象了。
最后我们还可以自己尝试一下,在block copy之后在block外直接打印objc,此时objc指针位置也与其他block中的objc同步了。
__forwarding的操作是在__main_block_copy_0函数当中的__Block_byref_copy()函数中实现的。
参考文献
关注公众号,回复26H49获取block源码