type
status
date
slug
summary
tags
category
icon
password

一、本质

如果用一句话概括,Block是一个将函数及其执行上下文封装起来的对象

1.1 clang说Block是个对象

  • 是对象:其内部第一个成员为isa指针;
  • 封装了函数调用:Block内代码块,封装了函数调用,调用Block,就是调用该封装的函数;
  • 执行上下文:Block还有一个描述Desc,该描述对象包含了Block的信息以及捕获变量的内存相关函数,及Block所在的环境上下文;
基于此,绘制了下图:
notion image

1.2 验证

有以下的Block:
重写xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc BlockStruct.m,我们得到C++源码:

1.2.1 test函数

test 函数将转换为为下面代码:
test函数转换为_I_BlockStruct_test,在该函数内部,block转换的结构体为:__BlockStruct__test_block_impl_0

1.2.2 block结构

block结构体__BlockStruct__test_block_impl_0有两个成员变量:
  • __block_impl:block结构体
  • __BlockStruct__test_block_desc_0:block描述的结构体
根据构造函数,再回过来看,如何初始化helloBlock的:
__BlockStruct__test_block_func_0赋值给impl__BlockStruct__test_block_desc_0_DATA赋值给Desc

1.2.3 __block_impl结构

上面说的block内部代码,此处为 NSLog(@"Hello world");,封装成了__block_impl类型,传入block结构体并初始化。
下面就是block内部封装的函数实现,后文我们将其称为Block的Func

1.2.4 block Desc结构

Block Desc描述的block的信息,包括Block大小和保留字。
后文将其称为Block的Desc

1.2.5 调用

在分析了- (void)test 函数的结构,以及block对应的结构组成,我们再看下是如何调用block的:
  • helloBlock结构的第一个成员变量为__block_impl,所以helloBlock首地址,就是__block_impl impl的首地址,即可以直接转换为__block_impl类型
  • *(void (*)(__block_impl )) 是__block_impl 中Func的类型
  • *((__block_impl *)helloBlock)->FuncPtr()** 调用函数
  • *((__block_impl *)helloBlock)** 函数参数

二、变量截获

Block本质是一个对象,那么在Block中访问全局变量以及局部变量,这个对象又是怎么处理这些变量的呢。
我们将变量分为以下几种类型,在以下表格中的存储区域,更多可参考内存管理(一)引入
类型
局部变量
全局变量
成员变量
定义
☞ 先定义再初始化,☞ 定义同时初始化
☞ 先定义再初始化☞ 定义同时初始化
不能在定义的同时进行初始化
访问
函数(方法)或者大括号内部访问
文件内直接访问
通过对象来访问
存储
全局存储区(static)text(const)
全局存储区(static)text (const)
内存管理
☞ 栈中数据系统管理,会自动释放。☞ 全局存储区和Text在程序运行中一直保留
☞ 全局存储区和Text在程序运行中一直保留
☞ ARC会自动管理对象的内存☞ MRC下手动管理
其他
成员变量不能离开类,离开类之后就不是成员变量
在此,需要特别指出static修饰的静态变量:
  • static修饰变量,都存放在全局存储区,根据是否初始化,分别存储在data段或bss中,在程序运行中一直保留;
  • static修饰局部变量,其作用域为函数或方法内
  • static修饰全局变量,其作用域为该文件内
另外,上面指出的区域如下:
  • .bss:存放未初始化数据的全局变量,以及 static 修饰的变量;
  • .data:存放已经初始化的的全局变量,以及 static 修饰的变量;
  • .text:存放代码,以及 const 等常量,这种常量包括 const 修饰符所修饰的,以及常量字符串。
我们在本文讨论Block对象捕获基本类型的情形。另外需要提醒的是,在捕获成员变量,即访问对象中属性或直接访问对象中成员变量的情形下,Block对象会做另外的处理。我们在后面篇章Block(四)对象类型的auto变量中讨论。

2.1 各种变量如何被捕获

我们下面见根据变量修饰符,来探查Block如何捕获不同修饰符的类型变量。
  • auto:自动变量修饰符
  • static:静态修饰符
  • const:常量修饰符
在这三种修饰符,我们又细分为全局变量和局部变量
我们针对变量的类型,重写成C++结构如下:
notion image
并得到如下结论:
notion image
在Block对象中捕获变量的类型基于变量类型,注意在局部变量中的异数:static变量
  • auto变量捕获后,Block中变量的类型和变量原类型一致;
  • 如变量是int类型,那么Block中:
  • static变量捕获后,Block对应的变量是对应变量的指针类型;
  • 如变量是int类型,那么Block中:

2.2 auto

auto变量,其实就是我们平时默认在方法内部定义的变量,在此,我们定义了一个:
  • 全局变量height
  • 局部变量weight
再看重新编译C++后personInfoBlock的结构体
我们可以看出,auto变量被捕获到Block的结构体中。
看下personInfoBlock内部代码封装的函数,及Block Desc描述信息:
可以看出:
  • 描述Block的Desc结构体未发生变化
  • Block Func结构也未发生变化
  • 其中Func内部使用到的变量weight,用了在初始化Block时传入的变量,即值传递
auto int weight = 66;
  • height变量为全局变量,直接访问
作为对比,我们将再看看bmiBlock有参数的情况:
可以看出,bmiBlock这种带有参数的情况下,Block并不会捕获变量,而是在使用时候,即时将新参数传入完成调用,Block最终转换的结构体和无参数且无变量的Block是非常相似。

2.3 static

下面我们定义了三个变量:
  • 全局
  • 变量:vision
  • 局部
  • 常量:height
  • 变量:weight
经过测试:
  1. 上述代码,结果输出为:vision is 4, height is 170, weight is 70
  1. 注释第7、8行代码,输出:vision is 3, height is 170, weight is 80
  1. 接上面,再注释11、12行,很明显输出:vision is 5, height is 170, weight is 60
从以上测试结果我们可以得出:
  • Block对象能获取最新的静态全局变量和静态局部变量;
  • 静态局部常量由于值不会更改,故没有变化;
我们来看一下,发生了什么。
为什么能获取static变量最新的值?从上面的转换中得出:
  1. static修饰的,其作用区域不管是全局还是局部,不管是常量还是变量,均存储在全局存储区中,存在全局存储区,该地址在程序运行过程中一直不会改变,所以能访问最新值。
  1. static修饰后:
  • 全局变量,直接访问
  • 局部变量,指针访问。其中在局部变量中,又有局部静态常量,即被const修饰的。
  • const存放在text段中,即使被static同时修饰,也存放text中的常量区;

2.4 const

如下定义:
  • const全局变量:vision
  • const局部变量:height
转换后:
从上面看出:
  • const全局变量直接访问
  • const局部变量,其实仍然是auto修饰,值传递

参考

链接

  1. Block Implementation Specification
  1. Blocks Programming Topics
  1. 通过逆向深入理解 Block 的内存模型
示例代码
  1. Block本质与变量截获