Block
简介
block
是将函数及其执行上下文封装起来的一个对象
在block
实现的内部,有很多变量,因为block
也是一个对象
其中包含了诸如isa
指针,imp
指针等对象变量,还有储存其截获变量的对象等
定义和使用 block
根据有无参数和有无返回值有以下几种简单使用方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void (^ BlockOne)(void ) = ^(void ){ NSLog (@"无参数,无返回值" ); }; BlockOne(); void (^BlockTwo)(int a) = ^(int a){ NSLog (@"有参数,无返回值, 参数 = %d," ,a); }; BlockTwo(100 ); int (^BlockThree)(int ,int ) = ^(int a,int b){ NSLog (@"有参数,有返回值" ); return a + b; }; BlockThree(1 , 5 ); int (^BlockFour)(void ) = ^{ NSLog (@"无参数,有返回值" ); return 100 ; }; BlockFour();
可是以上四种block
底层又是如何实现的呢? 其本质到底如何? 接下来我们一起探讨一下
Block的本质
为了方便我们这里新建一个Command Line Tool
项目, 在main
函数中执行上述中一个block
探索Block
的本质, 就要查看其源码, 这里我们使用下面命令把main.m
文件生成与其对应的c++
代码文件
在main.m
文件所在的目录下, 执行如下命令, 会生成一个main.cpp
文件
把main.cpp
文件添加到项目中, 并使其不参与项目的编译, 下面我们就具体看一下block
的底层到底是如何实现的
1 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
打开main.cpp
文件, 找到文件最底部, 可以看到block
的相关源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0 ) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { NSLog ((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_11c959_mi_0); } static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0 , sizeof (struct __main_block_impl_0)}; int main(int argc, const char * argv[]) { { __AtAutoreleasePool __autoreleasepool; void (* BlockOne)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); ((void (*)(__block_impl *))((__block_impl *)BlockOne)->FuncPtr)((__block_impl *)BlockOne); } return 0 ; } static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0 , 2 };
其中block
的声明和调用的对应关系如下
删除其中的强制转换的相关代码后
1 2 3 4 5 6 7 8 void (* BlockOne)(void ) = &__main_block_impl_0( (void *)__main_block_func_0, &__main_block_desc_0_DATA ); BlockOne->FuncPtr(BlockOne);
上述代码中__main_block_impl_0
函数接受两个参数, 并有一个返回值, 最后把函数的地址返回给BlockOne
, 下面找到__main_block_impl_0
的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0 ) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
__main_block_impl_0
函数中的第一个参数__main_block_func_0
赋值给了fp
, fp
又赋值给了impl.FuncPtr
, 也就意味着impl.FuncPtr
中存储的就是我们要执行的__main_block_func_0
函数的地址
Block
结构体中的isa
指向了_NSConcreteStackBlock
, 说明Block
是一个_NSConcreteStackBlock
类型, 具体后面会详解
__main_block_impl_0
函数中的第二个参数__main_block_desc_0_DATA
1 2 3 4 static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0 , sizeof (struct __main_block_impl_0)};
其中reserved
赋值为0
Block_size
被赋值为sizeof(struct __main_block_impl_0)
, 即为__main_block_impl_0
这个结构体占用内存的大小
__main_block_impl_0
的第二个参数, 接受的即为__main_block_desc_0
结构体的变量(__main_block_desc_0_DATA
)的地址
Block变量捕获
局部变量分为两大类: auto
和static
auto
: 自动变量, 离开作用域就会自动销毁, 默认情况下定义的局部变量都是auto
修饰的变量, 系统都会默认给添加一个auto
auto
不能修饰全局变量, 会报错
static
作用域内修饰局部变量, 可以修饰全局变量
全局变量
局部变量 auto变量捕获
auto
局部变量在Block
中是值传递
下述代码输出值为多少?
1 2 3 4 5 6 7 8 9 int age = 10 ;void (^BlockTwo)(void ) = ^(void ){ NSLog (@"age = %d," ,age); }; age = 13 ; BlockTwo();
输出值为什么是10而不是13呢? 我们还是生成main.cpp
代码看一下吧, 相关核心代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; // 这里多了一个age属性 int age; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int age = __cself->age; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_80d62b_mi_0,age); } static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; // 定义属性 int age = 10; // block的定义 void (*BlockTwo)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); // 改变属性值 age = 13; // 调用block ((void (*)(__block_impl *))((__block_impl *)BlockTwo)->FuncPtr)((__block_impl *)BlockTwo); } return 0; } static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
那么下面我们一步步看一下, 吧一些强制转换的代码去掉之后
1 2 3 4 5 6 7 8 9 10 int age = 10 ;void (*BlockTwo)(void ) = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA, age ); age = 13 ; BlockTwo->FuncPtr(BlockTwo);
在上面的__main_block_impl_0
函数里面相比于之前的, 多了一个age
参数
1 2 3 4 5 6 7 8 9 10 11 12 13 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0 ) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
上面的构造方法__main_block_impl_0
中, 多了一个_age
参数
同时后面多了一条age(_age)
语句, 在c++
中, age(_age)
相当于age = _age
, 即给age
属性赋值, 存储构造函数传过来的age
属性的值
所以在后面调用block
的时候, block
对应的结构体所存储的age
属性的值仍然是10, 并没有被更新
1 2 3 4 5 6 7 8 9 10 11 12 age = 13 ; BlockTwo->FuncPtr(BlockTwo); static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int age = __cself->age; NSLog ((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_80d62b_mi_0,age); }
static变量捕获
static
局部变量在Block
中是指针传递, 看一下下面代码的输出情况
1 2 3 4 5 6 7 8 9 10 auto int age = 10 ; static int weight = 20 ;void (^BlockTwo)(void ) = ^(void ){ NSLog (@"age = %d, weight = %d," ,age, weight); }; age = 13 ; weight = 23 ; BlockTwo();
上面代码输出结果: age = 10, weight = 23
重新赋值后age
的结果不变, 之前已经说过了
可是weight
的结果却是赋值后的结果, 至于为什么, 请继续向下看吧…
我们还是生成main.cpp
代码看一下吧, 相关核心代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; int *weight; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0 ) : age(_age), weight(_weight) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int age = __cself->age; int *weight = __cself->weight; NSLog ((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_282a93_mi_0,age, (*weight)); } static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0 , sizeof (struct __main_block_impl_0)}; int main(int argc, const char * argv[]) { { __AtAutoreleasePool __autoreleasepool; auto int age = 10 ; static int weight = 20 ; void (*BlockTwo)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &weight)); age = 13 ; weight = 23 ; ((void (*)(__block_impl *))((__block_impl *)BlockTwo)->FuncPtr)((__block_impl *)BlockTwo); } return 0 ; } static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0 , 2 };
从上面代码可以看到__main_block_impl_0
类中多了两个成员变量age
和weight
, 说明两个变量我们都可以捕获到
不同的是, 同样都是int
变量, 使用不同的修饰词修饰, __main_block_impl_0
类中也是不同的
static
修饰的变量weight
在block
中存储的是weight
的地址, 在后面的block
函数中我们使用的也是其地址
1 2 3 4 5 6 7 8 9 10 11 12 13 int age;int *weight;void (*BlockTwo)(void ) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age, &weight); __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0 ) : age(_age), weight(_weight) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }
也就是说上面的构造函数中
age
保存的是一个准确的值
weight
保存的是weight
所在的内存地址
所以在最后调用block
内部逻辑的时候
1 2 3 4 5 6 7 8 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int age = __cself->age; int *weight = __cself->weight; NSLog ((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_282a93_mi_0,age, (*weight)); }
也就是说, 同样是局部变量
auto
修饰的变量在block
中存储的是变量的值(值传递)
static
修饰的变量在block
中存储的是变量的内存地址(地址传递)
全局变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int age = 10 ;static int weight = 20 ;int main(int argc, const char * argv[]) { @autoreleasepool { void (^BlockTwo)(void ) = ^(void ){ NSLog (@"age = %d, weight = %d," ,age, weight); }; age = 13 ; weight = 23 ; BlockTwo(); } return 0 ; }
上面代码的输出结果, 毫无疑问是13和23, 相关c++
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 int age = 10 ;static int weight = 20 ;struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0 ) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { NSLog ((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_0ee0bb_mi_0,age, weight); } static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0 , sizeof (struct __main_block_impl_0)}; int main(int argc, const char * argv[]) { { __AtAutoreleasePool __autoreleasepool; void (*BlockTwo)(void ) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); age = 13 ; weight = 23 ; ((void (*)(__block_impl *))((__block_impl *)BlockTwo)->FuncPtr)((__block_impl *)BlockTwo); } return 0 ; } static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0 , 2 };
从上面代码可以看出__main_block_impl_0
结构体重并没有捕获到age
和weight
的成员变量
同样在定义block
变量的时候中也不需要传入age
和weight
的变量
在封装了block
执行逻辑的函数中, 就可以直接使用全局的变量即可
Block的类型 Block的三种类型
在之前的C++
源码中, __main_block_impl_0
结构体中isa
指向的类型是_NSConcreteStackBlock
下面就具体看一下, Block
的只要类型有那些
先看一下下面这部分代码的输出结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void (^block)(void) = ^(void){ NSLog(@"Hello World"); }; NSLog(@"%@", [block class]); NSLog(@"%@", [[block class] superclass]); NSLog(@"%@", [[[block class] superclass] superclass]); NSLog(@"%@", [[[[block class] superclass] superclass] superclass]); /* 2019-06-24 15:46:32.506386+0800 Block[3307:499032] __NSGlobalBlock__ 2019-06-24 15:46:32.506578+0800 Block[3307:499032] __NSGlobalBlock 2019-06-24 15:46:32.506593+0800 Block[3307:499032] NSBlock 2019-06-24 15:46:32.506605+0800 Block[3307:499032] NSObject */
block
的类型NSBlock
最终也是继承自NSObject
这也可以解释为什么block
的结构体__main_block_impl_0
中会有一个isa
指针了
此外, block
共有三种类型, 可以通过调用class
方法或者isa
指针查看具体类型, 最终都是继承自NSBlock
类型
__NSGlobalBlock__
或者_NSConcreteGlobalBlock
__NSStackBlock__
或者_NSConcreteStackBlock
__NSMallocBlock__
或者_NSConcreteMallocBlock
block在内存中的分配
_NSConcreteGlobalBlock
: 在数据区域
_NSConcreteStackBlock
: 在栈区域
_NSConcreteMallocBlock
: 在堆区域
应用程序的内存分配图如上图所示, 自上而下依次为内存的低地址–>内存的高地址
程序区域: 代码段, 用于存放代码
数据区域: 数据段, 用于存放全局变量
堆: 动态分配内存,需要程序员自己申请,程序员自己管理, 通常是alloc
或者malloc
方式申请的内存
栈: 用于存放局部变量, 系统会自动分配内存, 自动销毁内存
区分不同的block类型
上面提到, 一共有三种block
类型, 且不同的block
类型存放在内存的不同位置
但是如何区分所定义的block
到底是哪一种类型呢 看看下面代码的执行情况, 运行环境实在MRC
环境下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static int age = 10 ;int main(int argc, const char * argv[]) { @autoreleasepool { int weight = 21 ; void (^block1)(void ) = ^(void ){ NSLog (@"Hello World" ); }; void (^block2)(void ) = ^(void ){ NSLog (@"age = %d" , age); }; void (^block3)(void ) = ^(void ){ NSLog (@"age = %d" , weight); }; NSLog (@"block1 = %@" , [block1 class ]); NSLog (@"block2 = %@" , [block2 class ]); NSLog (@"block3 = %@" , [block3 class ]); } return 0 ; }
针对各种不同的block
总结如下
block
类型
环境
__NSGlobalBlock__
没有访问auto变量
__NSStackBlock__
访问了auto变量
__NSMallocBlock__
__NSStackBlock__
调用了copy
由于__NSMallocBlock__
是放在堆区域
要想创建出__NSMallocBlock__
类型的block
, 我们可以调用copy
方法
1 2 3 4 5 6 7 8 9 10 void (^block3)(void ) = ^(void ){ NSLog (@"age = %d" , weight); }; NSLog (@"block3 = %@" , [block3 class ]);NSLog (@"block3 = %@" , [[block3 copy ] class ]);
从上面的代码中我们可以明显看到, __NSStackBlock__
类型的block
调用copy
方法后, 就会变成__NSMallocBlock__
类型的block
相当于生成的block
是在堆区域的
那么另外两种类型调用copy
方法后,又会如何? 下面一起来看一下吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int weight = 21 ;void (^block1)(void ) = ^(void ){ NSLog (@"Hello World" ); }; void (^block3)(void ) = ^(void ){ NSLog (@"age = %d" , weight); }; NSLog (@"block1 = %@" , [block1 class ]);NSLog (@"block1 = %@" , [[block1 copy ] class ]);NSLog (@"block3 = %@" , [block3 class ]);NSLog (@"block3 = %@" , [[block3 copy ] class ]);NSLog (@"block3 = %@" , [[[block3 copy ] copy ] class ]);
从上面的代码可以看到, 只有__NSStackBlock__
类型的block
调用copy
之后才会变成__NSMallocBlock__
类型, 其他的都是原类型
主要也是__NSStackBlock__
类型的作用域是在栈中, 作用域中的局部变量会在函数结束时自动销毁
__NSStackBlock__
调用copy
操作后,分配的内存地址相当于从栈复制到堆;副本存储位置是堆
其他的则可参考下面表格
Block类
副本源的配置存储域
复制效果
__NSStackBlock__
栈
从栈复制到堆
__NSGlobalBlock__
程序的数据区域
什么也不做
__NSMallocBlock__
堆
引用计数增加
在ARC
环境下, 编译器会根据情况自动将站上的block
复制到堆上, 类似以下情况
block
作为函数返回值时
将block
赋值给__strong
修饰的指针时
block
作为GCD
的方法参数时
__block修饰符 Question: 定义一个auto修饰的局部变量, 并在block
中修改该变量的值, 能否修改成功呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 auto int width = 10 ; static int height = 20 ;void (^block)(void ) = ^(void ){ width = 22 ; height = 22 ; NSLog (@"width = %d, height = %d" , width, height); }; block();
在之前提到, 在block
中, auto
修饰的变量是值传递
static
修饰的变量是指针传递, 所以在上述代码中, block
存储的只是height
的内存地址
同样auto
变量实在main
函数中定义的, 而block
的执行逻辑是在__main_block_func_0
结构体的方法中执行的, 相当于局部变量不能跨函数访问
至于static
修饰的变量为什么可以修改?
在__main_block_impl_0
结构体中height
存储的是其内存地址, 在其他函数或者结构体中访问和改变height
的方式都是通过其真真访问的
类似赋值方式: (*height) = 22;
取值方式: (*height)
__block修饰auto变量 1 2 3 4 5 6 7 8 9 10 __block auto int width = 10 ; void (^block)(void ) = ^(void ) { width = 12 ; NSLog (@"width = %d" , width); }; block();
为什么上面的代码就可以修改变量了呢, 这是为什么呢…..请看源码
下面是生成的block
的结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_width_0 *width; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_width_0 *_width, int flags=0 ) : width(_width->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
上述代码看到__block
可以用于解决block
内部无法修改auto
修饰的变量值得问题
但是__block
不能修饰全局变量和static
修饰的静态变量(同样也不需要, 因为在block
内部可以直接修改)
经过__block
修饰的变量会被包装成一个对象(__Block_byref_width_0
)
下面是width
被包装后的对象的结构体, 在结构体内, 会有一个width
成员变量
1 2 3 4 5 6 7 8 9 struct __Block_byref_width_0 { void *__isa; __Block_byref_width_0 *__forwarding; int __flags; int __size; int width; };
下面我们先看一下, auto
和block
的定义和调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int main(int argc, const char * argv[]) { { __AtAutoreleasePool __autoreleasepool; auto __Block_byref_width_0 width = { 0 , &width, 0 , sizeof (__Block_byref_width_0), 10 }; void (*block)(void ) = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA, &width, 570425344 ); block->FuncPtr(block); } return 0 ; }
可以看到在定义的__Block_byref_width_0
类型的width
中的每一个参数分别赋值给了__Block_byref_width_0
结构体中的每一个成员变量
而在block
内部重新对width
重新赋值的逻辑中
1 2 3 4 5 6 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_width_0 *width = __cself->width; (width->__forwarding->width) = 12 ; NSLog ((NSString *)&__NSConstantStringImpl__var_folders_ty_804897ld2zg4pfcgx2p4wqh80000gn_T_main_9241d5_mi_0, (width->__forwarding->width)); }
上面代码中的width
是一个__Block_byref_width_0
类型的变量
width
对象通过找到内部的__forwarding
成员变量
在__Block_byref_width_0
结构体中__forwarding
是一个指向自己本身的成员变量
所以最后再通过__forwarding
找到__Block_byref_width_0
的成员变量width
, 在进行重新赋值
在NSLog
中也是通过这种逻辑获取width
的值