Block原理(一)

手机软件开发 2024-9-13 13:27:37 151 0 来自 中国
Block究竟是什么,我们先从c++代码开始
从一个最简朴的block布局开始

clang -rewrite-objc main.m -o main.cpp && open main.cpp
为了方便阅读 我们简化一下代码
为了方便进一步阅读,这里对此中的定名做了简化,参考下面的简朴流程


  • 团结clang编译中央c++代码,通过block的创建,团结上图, 脑筋里先勾勒一个sketch

    • 创建两层布局

      • BlockCreate 布局
      • Block布局,属于BlockCreate的成员

    • 通过BlockCreate构造传参,实例化BlockCreate成员Block::block
    • 终极返回的是一个 BlockCreate布局指针
    • 通过BlockCreate布局首地点,我们可以拿到成员Block::block, BlockCreate首地点与 成员block首地点是一样的,由于block位于 BlockCreate内存空间的起始处
      既然可以拿到首地点(成员block地点),那么同样可以通过内存偏移拿到成员Desc的地点
    • 通过拿到成员Block::block地点,就可以调用Block::block的成员方法FuncPtr了, 而FuncPtr恰恰是通过 BlockCreate构造实例化Block::block成员时 赋值的fun函数入口地点



  • 肯定要相识这个两层布局,固然不是真正意义源码,但是对后面我们分析源码很有资助
前面的例子没有使用变量,我们可以通过前面的方式再操纵一次,对比下区别
6.png 当自界说的布局体内部访问外部的一个局部变量时
你会发现clang天生的c++代码发生了变革 对照上面的谁人实例化流程

  • BlockCreate 布局体内多了一个成员 int a
  • BlockCreate构造 也多了一个传参
  • func函数内部访问变量 a通过  func(BlockCreate self) 的 BlockCreate::self 来获取拷贝
  • 你会发现有3处存在变量a

    • main函数内的局部变量a
    • BlockCrete 布局体内的成员变量a
    • func方法内部的局部变量a
    着实这3个变量a分别是3个差异的变量了

把局部变量a改为static修饰,继承clang c++查看

用static修饰变量a,不一样了
BlockCreate构造传参,此时通报的是  a的地点,而BlockCreate成员 a也酿成了 指针, func内部的局部变量a 也酿成了 指针,func内部的a是通过 BlockCreate::*self 的指针a 赋值 给func内部的局部变量 指针a
以是static修饰a后,func内部访问的a着实照旧 main函数内部的 指针a
把局部变量a改为 __block修饰,继承clang c++查看

渴望你不会以为懵,这次复杂了些

  • 出现了一个布局 __Block_byref_a_0
  • BlockCreate 成员Desc的布局内部多了两个 函数 copy & dispose
这里简朴解释下

  • 普通的局部变量a 酿成了一个布局 __Block_byref_a_0, a是这个布局的成员

    • 成员 void *__isa
    • 成员 __block_byref_a_0 *__forwarding;
    • 成员 int __flags;
    • 成员 int __size
    • 成员 int a
    __isa 从前面的截图可知。是一个_NSConcreteStackBlock 也就是栈block指针

在main里声明的__block修饰的局部变量, 地点赋值给了 __forwarding, 值赋给了 Block_byref布局里的成员a,注意这个设定, 固然成员也叫a,只是起到一个罗致值的作用,关键在于__forwarding 拿到了原来的a的指针先看下__block修饰的a究竟是怎么访问的
__forwarding 范例 __Block_byref_a_0 *,雷同于链表节点,以是也是一个指向 __Block_byref_a_0 布局的指针 至于有什么用,暂存疑,后面源码接着分析对比着看,着实很显着,不难明白
block源码 - libclosure-79 查看

源码入口该怎么查看呢,我们先通过汇编看下
既然retainBlock,分析block开发了空间,进入查看
继承跳转 br x16
现在找到了_Block_copy这样一个符号,然后进入源码查看
17.png 你会看到一个布局Block_layout
Block_layout 就是前面通过clang c++代码 分析出的 两层布局BlockCreate成员 Block::block
__block 修饰变量 测试代码放进 block源码举行调试

这段代码是在block源码中测试的
20.png 这着实就是依照Block_layout 栈上的空间布局,在堆区创建了一个Block_layout布局
同时 新开发的Block_layout布局->invoke 从原来栈上Block_layout->invoke拷贝过来
21.png 既然是堆上开发空间创建的Block_layout布局,自然isa 指向 _NSConcreteMallocBlock (堆block)
block分析源码碰到标题

现在尚有两块没探索到源码,就是 前面通过clang 编译天生的c++代码中__Block_byref_a_0这样的布局,尚有一块是BlockCreate构造逻辑部分
那么接下来该何去何从?
我选择最原始的方式 汇编 + 下符号断点 + 团结clang c++代码分析
先把代码断到此处,防止dyld其他流程干扰
23.png 下符号断点 同时把前面分析过的 _Block_copy 符号也下下来,为了方便分析流程
跟着调试 进入 _Block_object_dispose:
24.png 回到之前clang编译出的c++代码看下
25.png 既然下到了符号_Block_object_dispose 那么同样也把符号 _Block_object_copy下下来继承调试
没有的话 就试试 _Block_object_assign, 之以是没有找到 _Block_object_copy符号,是由于那是由编译器决定的
乐成断点符号 _Block_object_assign
找到头绪,自然我们又回到了源码


  • 看下源码解释
    When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point to do the assignment.
    当Blocks(可以明白为前面的有成员func的谁人布局) 大概 Block_byref持有对象时间,这个入口就会被触发 实行赋值操纵


  • __block int a = 10 范例为 BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK or BLOCK_FIELD_IS_BYREF
    实行 _Block_byref_copy()
_Block_byref_copy

在分析_Block_byref_copy流程之前,我们必要相识下Block_byref 是什么
29.png 从前面clang编译拿到的c++代码,可以看到,Block_byref 是对通例变量的封装,封装布局里还多了isa,__forwarding成员
30.png 源码中还存在 Block_byref_2  Block_byref_3 两个布局,临时不表,后面会继承分析
我们可以做个假设,现在我们测试的实例 是block引用外部 __block修饰的变量,我们也是这么用的,既然block内部访问外部变量,那么也会对于这个变量的引用计数产生影响  flags就是存储引用计数的
_Block_byref_copy翻译

31.png 假如源byref布局已经在heap上,则不必要实行拷贝,引用计数+1
中央有一段内存偏移的代码,还没解析,继承
byref_keep byref_destroy 究竟实现了什么功能
由于我们用的通例变量a测试 我们换成object看下
将变量a换为object测试

clang c++代码
35.png 从源码得知
37.png 38.png 131有什么意义
40.png 两个参数 + 40 什么意思
41.png 按照编译的逻辑,byref_keep 就是 object范例的对象的 拷贝
但是运行时会做修正 流程有差异
同样 byref_destroy:
42.png 以上为 Block_byref 逻辑,再通过clang得到的c++ 看下 Block_layout 的处置惩罚
43.png 再确认下 __block修饰的 object对象,在block体里 究竟是怎样访问的
总结


  • __block 修饰变量之后,编译器会在栈上构建一个 栈Block_byref(包含变量指针)
  • 界说block,可以明白为编译器天生一个中央布局BlockCreate(这个名字是特意起的,知道是个布局,为了便于明白,你可以这么明白)

    • 同时编译器会在栈上初始化构建一个 栈Block_layout(包含func成员)

  • 实行BlockCreate构造方法

    • 通过Block_layout首地点偏移 得到 Block_copy函数地点, 实行Block_copy,把 栈Block_byref 拷贝 到堆Block_byref
    • 构造参数 栈Block_byref,通过Block_byref首地点偏移 得到 Block_byref_2(包含_Block_byref_copy 即byref拷贝函数)首地点, 实行 _Block_byref_copy函数, 把栈Block_byref 拷贝到 堆Block_byref
    • 继承上一步的位置 内存偏移 8字节,得到堆上开发的 object内存空间首地点, 这里固然就存放 object对象了
    • 必要注意的一个细节 栈Block_byref 拷贝到 堆Block_byref之后,由于堆上是新的内存空间,那么栈与堆不就两个空间了吗,怎样保障访问的是同一块内存?
      就觉办法就是,在拷贝之后, 把 栈Block_byref 和 堆Block_byref 里的forwarding 都指向了 堆Block_byref, 也就是 堆 forwarding再指向一遍本身
      __block修饰变量之后,不管是在block块内访问变量,照旧在block外访问变量,都是通过 forwarding访问到堆空间,然后再访问目的空间内的 变量, 这样就保障了 访问的变量是同一块内存空间

47.png


  • Block_byref 持有的变量生命周期竣事,实行 _Block_object_dispose

    • 实行_Block_byref_release函数,根据Block_byref 首地点偏移 找到 Block_byref_2首地点,继承偏移8字节 得到 byref_destroy 实行析构 接纳堆内存空间

  • Block_layout 作用域竣事 大概 生命周期竣事, 实行 _Block_release

    • 根据 Block_layout 首地点偏移 找到 Block_descriptor_2 首地点,继承偏移8字节,的到 dispose 实行析构 接纳 堆上开发的 Block_layout 堆内存空间

读取寄存器查看

符号断点 _Block_copy
_Block_copy 实行之前,寄存器rax罗致参数 (arm64 读取寄存器x1)
实行完之后,ret返回,rax寄存器存储返回值


  • 变量a 改为 __block修饰
53.png 由于__block修饰,Block_layout 中就出现了 copy 函数地点,通过copy,就实行 _Block_copy
而没有__block修饰,没有 copy dispose 函数,默认实行 _Block_copy
造成这个差异就在于 构造传参时间  flag的区别,__block修饰之前 是0,__block修饰之后, 1 << 25
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2025-2-23 06:26, Processed in 0.184703 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表