iOS底层探索之Block(五)——Block源码分析(__block 底层都做了什么?)

回顾

在上一篇博客中,通过对block追根溯源,汇编跟踪调式,源码分析,对底层结构和 block的属性方法都有一定的认识, 那么本篇博客将继续对block的底层进行分析。

Block探索分析

iOS底层探索之Block(一)——初识Block(你知道几种Block呢?)

iOS底层探索之Block(二)——如何解决Block循环引用问题?

iOS底层探索之Block(三)——Block的本质

iOS底层探索之Block(四)——Block的探索和源码分析

1. block底层探索

block的结构和签名都分析完了,但是block最难的点还是怎么捕获到变量等。

cpp查看底层结构

再去瞄一眼底层结构,如下代码:

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSObject *objc = [NSObject alloc];
    void (^jp_block)(void) = ^{
        NSLog(@"zjpreno: %@ ",objc);
    };
    jp_block(); 
}

@end

通过 xcrun -sdk iphonesimulator clang -S -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.2 ViewController.m 命令生成.cpp文件

ViewController.cpp

通过生成的.cpp文件可以看到

  • __ViewController__viewDidLoad_block_copy_0是对应Block_descriptor_2里面的 copy
  • __ViewController__viewDidLoad_block_dispose_0是对应Block_descriptor_2里面的 dispose
  • __ViewController__viewDidLoad_block_copy_0方法里面怎么还多了_Block_object_assign这个玩意, __ViewController__viewDidLoad_block_dispose_0里面多了_Block_object_dispose,让人百思不得其解,先别急请继续往下看。

底层结构分析

cpp对应源码中的Block_descriptor信息

这个 cpp的结构体即对应源码中的Block_descriptor信息。

  • reservedsize对应Block_descriptor_1的两个属性。
  • void (*copy)void (*dispose)对应Block_descriptor_2的两个方法。
  • copy方法的实现中,会调用_Block_object_assign,此过程即为外部变量的捕获的方法。
  • dispose方法的实现中,会调用_Block_object_dispose,此过程为释放方法。

2. block源码分析

在源码中搜索_Block_object_assign,找到如下注释信息:

_Block_object_assign注释信息

Block 被复制到堆时,一个Block 可以引用四种不同的需要帮助的东西。

  1. 基于C++堆栈的对象
  2. Objective-C 对象的引用
  3. 其他块
  4. __block 变量

在这些情况下,辅助函数由编译器合成,用于 Block_copyBlock_release,称为复制处置辅助函数。 复制助手为基于 C++堆栈的对象发出对C++ const 复制构造函数的调用,并为其余调用运行时支持函数_Block_object_assigndispose helper调用 C++析构函数用于情况 1,调用 _Block_object_dispose用于其余情况。

  • 编译器在生成复制/处置助手时使用的运行时支持函数
  • _Block_object_assign()_Block_object_dispose() 参数的值
parameters

_Block_object_assign_Block_object_disposeflags 参数设置为:

  • BLOCK_FIELD_IS_OBJECT (3),对于Objective-C Object的情况
  • BLOCK_FIELD_IS_BLOCK (7),对于另一个 block 的情况
  • BLOCK_FIELD_IS_BYREF (8),对于__block变量的情况。

如果 __block 变量被标记为weak,则编译器也在 BLOCK_FIELD_IS_WEAK (16)

所以 Block copy/dispose helper 应该只生成 37824 四个标志值。

上面是源码的注释,那么现在去验证一下:

验证

cpp我们也可以看到对于 OC 对象,是 BLOCK_FIELD_IS_OBJECT (3),那么我们现在去加一下__block,看看会不会变成 BLOCK_FIELD_IS_BYREF (8)呢?
__block验证

看到没有,加了__block之后就变成 BLOCK_FIELD_IS_BYREF (8),还有谁,45 度仰望天花板,我这该死无处安放的魅力啊!
666

_Block_object_assign

  • _Block_object_assign
对比分析
  • 如果持有变量是BLOCK_FIELD_IS_OBJECT类型,即没有__block修饰,dest指针指向objec,引用计数加1,原本的对象地址给了block里面的目标对象,这样就都指向同一个地址空间
  • 如果是BLOCK_FIELD_IS_BLOCK类型,也就是block的类型,就是捕获到了 block则进行_Block_copy操作,把objec的内容复制一份到自己的里面,也就是拷贝到堆区
  • 如果是BLOCK_FIELD_IS_BYREF,即有__block修饰,则会调用_Block_byref_copy

_Block_byref_copy

  • _Block_byref_copy
_Block_byref_copy
  • 将外部对象封装成结构体Block_byref *src

  • 如果是有引用计数的正常对象,就对引用计数进行处理,调用malloc,生成一个Block_byref *copy,如果不是,则会调用memmove

  • 设置forwarding,保证block内部和外部都指向同一个对象

    forwarding

Block_byref_2

  • Block_byref_2
    Block_byref_2

    这里就是对 block捕获的变量进行处理了,调用了byref_keep对其生命周期进行保存。Block_byref的设计思路和Block_layoutdescriptor流程类似,通过byref->flag标识码判断对应的属性,以此来判断Block_byref_2是否存在,如下图所示:
    Block_byref

    如果我们的block捕获了使用__block修饰了外部变量,在cpp文件中,Block_byref结构体中就会默认生成两个方法,即对应Block_byref_2keep方法和destory方法,验证如下:
    cpp 验证Block_byref_2

cpp文件中这两个函数的实现如下:

keep方法和destory实现
  • _Block_object_assign
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);

(char*)dst + 40)相当于 objec,指针平移

对比

上面的 40 是怎么来的呢?如下:

内存大小

__Block_byref_id_object_copy_131方法的调用会调用_Block_object_assign函数,对Block_byref结构体中的对象进行BLOCK_FIELD_IS_OBJECT流程处理。

_Block_object_dispose

对于_Block_object_dispose方法,也就是释放流程,也是类似的。

_Block_object_dispose

也是对block类型的判断,再调用_Block_release(object)走释放流程。

_Block_release
  • flags parameter
flags parameter

3. 总结

block 的三层拷贝

  • 如果是__Block修饰的变量,会对block进行copy操作,从栈区拷贝到堆区
  • block捕获变量,对Block_byref结构体的拷贝
  • Block_byref会对传入的 objec进行拷贝,到此完成 block 的三层拷贝

更多内容持续更新

喜欢就点个赞吧

觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我

欢迎大家留言交流,批评指正,互相学习,提升自我

你可能感兴趣的:(iOS底层探索之Block(五)——Block源码分析(__block 底层都做了什么?))