Block
Block本质
In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.
Block
是将函数及其执行上下文封装起来的对象
Block
的调用即是函数的调用Block
本质上也是一个OC对象,它内部也有个isa指针Block
是封装了函数调用以及函数调用环境的OC对象
block地层结构图中的第一个成员就是一个isa指针,所以我们可以将block当成一个对象来看待。isa常见的就是_NSConcreteStackBlock
,_NSConcreteMallocBlock
,_NSConcreteGlobalBlock
这3种
Block 写法
@property (nonatomic, copy)void (^addBlockResult)(BOOL) ;
int multiplier = 6
int(^Block)(int) = ^int(int num){
return num * multiplier;
}
Block 结构
//如下代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void (^block)(int, int) = ^(int c, int b){
NSLog(@"I am a block!");
NSLog(@"I am a block!");
NSLog(@"c = %d",c);
NSLog(@"b = %d",b);
NSLog(@"a的值为%d",a);
};
block(50,100);
}
return 0;
}
通过
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
将OC文件用Clang重写
#import <Foundation/Foundation.h>
//将block的底层结构struct __main_block_impl_0直接般到main.m里面
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void (^block)(int, int) = ^(int c, int b){
NSLog(@"I am a block!");
NSLog(@"I am a block!");
NSLog(@"c = %d",c);
NSLog(@"b = %d",b);
NSLog(@"a的值为%d",a);
};
struct __main_block_impl_0 *tmpBlock = (__bridge struct __main_block_impl_0 *)block;
block(50,100);
}
return 0;
}
Block 底层代码
Block 捕获外部变量
Block 捕获基础类型
Block捕获auto变量
接下来看看这种情况
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
//Block的定义
void (^block)(void) = ^(){
NSLog(@"Age is %d", age);
};
//先修改age的值
age = 20;
//Block的调用
block();
}
return 0;
}
在block之前定义了一个int a = 10
,然后在block内部使用了这个age
,而且我在调用block之前,先将age
的值修改成了20
,那么此时程序运行会是什么结果呢
Interview03-block[4064:375528] Age is 10
Program ended with exit code: 0
结果是block中打印出的a
是10
,我们在block外部对age
的修改结果并没有对block的内部打印产生影响
(1)首先看一下此时block对应的结构体
我们发现有三处变化
- 新增了一个
int age
成员变量 - 构造函数里面多了一个参数
int _age
- 构造函数里面参数尾部多了一个
: age(_age)
,这是c++的语法,作用时将参数_age
自动赋值给成员变量age
(2)再看一下main函数中的block定义以及赋值的代码
在用block
构造函数生成block
的时候,使用了外部定义的 int a = 10
,因为c函数的参数都是值传递,所以这里是将此时外部变量a
的值10
传给了block
的构造函数__main_block_impl_0
,因此block内部的成员变量age
会被赋值成10
。
(3)再看一下block内部封装的函数
可以看到打印代码里面使用的age
,实际上就是block内部的成员变量age
,不是我们在外面定义的那个age
,因此,当block被赋值之后,其成员变量age
被赋值成了当时构造函数传进来的参数10
,所以最终打印出来值就是10
,不论外部的age
再如何的修改。外部的age
跟block的成员变量age
是两个不同的变量,互不影响。
其实,上面我门讨论的这个block外部变量age
是一个局部auto变量,也叫自动变量。除了auto变量
,C语言里面还有局部static变量
(静态变量)和全局变量,接下来我们就看看,Block对于这几种变量的使用,做了如何的处理。
Block捕获局部static变量
首先我们将上面的OC代码改造如下
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
static int height = 10;
//Block的定义
void (^block)(void) = ^(){
NSLog(@"Age is %d, height is %d", age, height);
};
//先修改age和height的值
age = 20;
height = 20;
//Block的调用
block();
}
return 0;
}
我们有增加了一个static
变量height
,并且在同样的地方修改height
的值,便于和之前的age
进行对比。首先运行代码看一下结果
Interview03-block[4725:476530] Age is 10, height is 20
Program ended with exit code: 0
可以看到,block输出的 height
值是我们在外部重新为其赋的20
。
(1)借用上面的分析流程一样,先看一下block对应的结构体
针对static变量height, block内部为其增加了一个int *height;
成员变量,构造函数里面对应的参数是int *_height
。看到这里这里要存储的是一个地址,该地址应该就是外部static
变量height
的地址值。
(2)main函数里的block赋值过程
block构造函数里面传入的,就是外部的这个height的地址值。
(3)block内部的函数
那么可以看到,block内部的函数也是通过block所存储的地址值*height
访问了外部的static
变量height
的值。
因此,当我们从外部修改height
的值之后,调用block打印出的height
的值也相应的改变了,因为block内部是通过 指针 引用了外部的这个static
变量height
。
对于auto
、static
变量,为什么block选择用不同方式处理它们呢?
一个自动变量(auto
)的存储空间位于函数栈空间上,在函数开辟栈空间时被创建,在函数结束时销毁,而block的调用时机有可能发生在函数结束之后的,因此就无法使用自动变量了,所以在block一开始定义赋值的过程里,就将自动变量的值拷贝到他自己的存储空间上。 而对于局部静态变量(static
),C语法下static
会改变所修饰的局部变量的生命周期,使其在 程序整个运行期间都存在 ,所以block选择持有它的指针,在block被调用时,通过该指针访问这个变量的内容就行。
Block使用全局变量
上面讨论block对于局部变量的处理,在看一看对于全局变量,情况又是如何
输出结果如下
Interview03-block[13997:1263406] Age is 20, height is 20
Program ended with exit code: 0
在通过命令行生成一下编译后的C++文件,同样还是在文件底部去看
block没有对全局变量进行捕获行为,只需要在要用的时候,直接通过变量名访问就行了,因为全局变量时跨函数的,可以直接通过变量的名字直接访问。 同样,者也帮我我们理解了为什么对于局部的变量,block需要对其采取“捕获”行为,正是因为局部变量定在与函数内部,无法跨函数使用,所以根据局部变量不同的存储属性,要么将其值直接进行拷贝(auto
),要么对其地址进行拷贝(static
)。
总结
局部变量会被block捕获
自动变量(
auto
),block通过值拷贝方式捕获,在其内部创建一个同类型变量,并且将自动变量的值拷贝给block的内部变量,block代码块执行的时候,直接访问它的这个内部变量。静态变量(
static
),block通过地址拷贝方式捕获,在其内部创建一个指向同类型变量的指针, 将静态变量的地址值拷贝给block内部的这个指针,block代码块执行的时候,通过内部存储的指针间接访问静态变量。全局变量不会被block捕获, block代码块执行的时候,通过全局变量名直接访问。
Block对于self的处理
编译结果显示block对self
进行了捕获。But why? 我们知道,图中的block位于test
方法里面,实际上任何的oc方法,转换成底层的c函数,里面都有两个默认
的参数,self
和 _cmd
所以作为函数默认参数的self
的实际上也是该函数的局部变量,根据我们上面总结的原则,只要是局部变量,block都会对其进行捕获,这就解释通了。
下面的情况呢
先看编译结果
看得出来,还是进行了捕获,在图中标明的黄色框框,就很好理解了,block最终访问CLPerson
的成员变量_age
的时候,是通过self
+_age偏移量
,获得_age
的地址后从而进行间接访问的,所以在oc代码中,_age
的写法等同与self->_age
,说白了,这里还是需要用到self
,因此block还是需要对self
进行捕获的。
Block捕获对象类型
有以下代码:
typedef void(^CLBlock)(void);//➕➕➕
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLBlock myBlock;
{//临时作用域开始
CLPerson *person = [[CLPerson alloc] init];
person.age = 10;
myBlock = ^{
NSLog(@"---------%d",person.age);
};
}//临时作用域结束
NSLog(@"-----------flag1");
}
return 0;
}
由于现在是ARC环境,myBlock
属于强指针,因此在将block对象赋值给myBlock
指针的时候,编译器会自动对block对象执行copy
操作,因此赋值完成后,myBlock
指向的是一个堆空间上的block对象副本
通过Clang重写
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
CLPerson *person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, CLPerson *_person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
CLPerson *person = __cself->person; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__p19yp82j0xd2m_1k8fpr77z40000gn_T_main_2cca58_mi_0,((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("age")));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0,
__main_block_dispose_0
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
CLBlock myBlock;
{
CLPerson * person = objc_msgSend(objc_msgSend(objc_getClass("CLPerson"),
sel_registerName("alloc")
),
sel_registerName("init")
);
objc_msgSend(person,
sel_registerName("setAge:"),
30
);
myBlock = objc_msgSend(&__main_block_impl_0(__main_block_func_0,
&__main_block_desc_0_DATA,
person,
570425344),
sel_registerName("copy")
);
}
}
return 0;
}
__main_block_desc_0
结构体里面多了两个彩蛋
- 函数指针
copy
,也就是__main_block_copy_0()
,内部调用了_Block_object_assign()
- 函数指针
dispose
,也就是__main_block_dispose_0()
,内部调用了_Block_object_dispose()
ARC 下CLPerson *person
被认为是强指针,等价于_strong CLPerson *person
,而弱指针需要显式地表示为__weak CLPerson *person
。通过终端命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m -o main.cpp
,可以看到block
的内捕获到的person
指针如下
为了对比,我们再分别看一下下面三种 场景分别是什么情况的:
- ARC环境-->堆上的
block
-->弱指针__weak CLPerson *person
- ARC环境-->栈上的
block
-->强指针CLPerson *person
- ARC环境-->栈上的
block
-->弱指针__weak CLPerson *person
【ARC环境-->堆上的block
-->弱指针__weak CLPerson *person
】 案例如下
***********************main.m*************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"
typedef void(^CLBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLBlock myBlock;
{//临时作用域开始
__weak CLPerson * person = [[CLPerson alloc] init];
person.age = 30;
myBlock = ^{
NSLog(@"---------%d",person.age);
} ;
}//临时作用域结束
NSLog(@"-------------");
}
NSLog(@"------main autoreleasepool end-------");
return 0;
}
block的底层结构如下
运行结果显示堆上的block使用弱指针__weak CLPerson *person
,没有影响person
所指向对象的生命周期,出了临时作用域的之后就被释放了。
【ARC环境-->栈上的block
-->强指针CLPerson *person
】
***********************main.m*************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"
typedef void(^CLBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLBlock myBlock;
{//临时作用域开始
CLPerson * person = [[CLPerson alloc] init];
person.age = 30;
^{
NSLog(@"---------%d",person.age);
} ;
}//临时作用域结束
NSLog(@"-------------");
}
NSLog(@"------main autoreleasepool end-------");
return 0;
}
block底层结构如下
运行结果显示栈上的block使用强指针CLPerson *person
,没有影响person
所指向对象的生命周期,出了临时作用域的之后就被释放了。
【ARC环境-->栈上的block
-->弱指针__weak CLPerson *person
】
***********************main.m*************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"
typedef void(^CLBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
CLBlock myBlock;
{//临时作用域开始
__weak CLPerson * person = [[CLPerson alloc] init];
person.age = 30;
^{
NSLog(@"---------%d",person.age);
} ;
}//临时作用域结束
NSLog(@"-------------");
}
NSLog(@"------main autoreleasepool end-------");
return 0;
}
block底层结构为
运行结果显示栈上的block使用弱指针__weak CLPerson *person
,没有影响person
所指向对象的生命周期,出了临时作用域的之后就被释放了。
Block类型
Block有3种类型
回顾一下程序的内存布局
- 代码段 占用空间很小,一般存放在内存的低地址空间,我们平时编写的所有代码,就是放在这个区域
- 数据段 用来存放全局变量
- 堆区 是动态分配内存的,用来存放我们代码中通过alloc生成的对象,动态分配内存的特点是需要程序员申请内存和管理内存。例如OC中alloc生成的对象需要调用releas方法释放【MRC下】,C中通过malloc生成的对象必须要通过free()去释放。
- 栈区 系统自动分配和销毁内存,用于存放函数内生成的局部变量
(1) NSGlobalBlock(也就是_NSConcreteGlobalBlock)
如果一个block内部没有使用/访问 自动变量(auto变量),那么它的类型即为
__NSGlobalBlock__
,它会被存储在应用程序的 数据段
(2) NSStaticBlock(也就是_NSConcreteStaticBlock)
如果一个block有使用/访问 自动变量(auto变量),那么它的类型即为
__NSStaticBlock__
,它会被存储在应用程序的 栈区
(3) NSMallocBlock(也就是_NSConcreteMallocBlock)
对
__NSMallocBlock__
调用copy
方法,就可以转变成__NSMallocBlock__
,它会被存储在堆区上
总结
对每一种类型的block调用copy后的结果如下
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,例如以下的情况
- block作为函数参数返回的时候
- 将block赋值给
__strong
指针的时候 - block作为Cocoa API中方法名里面含有
usingBlock
的方法参数时 - block作为GCD API的方法参数的时候
Block生命周期
NSConcreteStackBlock
是由编译器自动管理,超过作用域之外就会自动释放了。而 NSConcreteMallocBlock
是由程序员自己管理,如果没有被强引用也会被消耗。NSConcreteGlobalBlock
由于存在于全局区,所以会一直伴随着应用程序。
无论是MAC还是ARC
- 当block为
__NSStackBlock__
类型时候,是在栈空间,无论对外面使用的是strong 还是weak 都不会对外面的对象进行强引用 - 当block为
__NSMallocBlock__
类型时候,是在堆空间,block是内部的_Block_object_assign
函数会根据strong
或者weak
对外界的对象进行强引用或者弱引用。
其实也很好理解,因为block本身就在栈上,自己都随时可能消失,怎么能保住别人的命呢?
- 当block内部访问了对象类型的auto变量时
- 如果block是在栈上,将不会对auto变量产生强引用
- 如果block被拷贝到堆上
- 会调用block内部的copy函数
- copy函数内部会调用
_Block_object_assign
函数 _Block_object_assign
函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)
做出相应的操作,形成强引用(retain)或者弱引用
- 如果block从堆上移除
- 会调用block内部的dispose函数
- dispose函数内部会调用
_Block_object_dispose
函数 _Block_object_dispose
函数会自动释放引用的auto变量(release)
函数 | 调用时机 |
---|---|
copy函数 | 栈上的Block复制到堆上 |
dispose函数 | 堆上的block被废弃时 |
weak的实现原理
在原对象释放之后,weak对象就会变成null,防止野指针。所以就输出了null了。
那么我们怎么才能在weakSelf之后,block里面还能继续使用weakSelf之后的对象呢?
究其根本原因就是weakSelf之后,无法控制什么时候会被释放,为了保证在block内不会被释放,需要添加_strong。
在block里面使用的_strong修饰的weakSelf是为了在函数生命周期中防止self提前释放。strongSelf是一个自动变量当block执行完毕就会释放自动变量strongSelf不会对self进行一直进行强引用。
__block 修饰符
__block修饰符原理:
编译器会将__block
变量包装成一个结构体__Block_byref_age_0
,结构体内部*__forwarding
是指向自身的指针,内部还存储着外部auto变量
的值
一开始,栈空间的block有一个__Block_byref_a_0
结构体,
指向外部__Block_byref_a_0
的地址,
其中它的__forwarding指针指向自身,
当block从栈copy到堆时,
堆空间的block有一个__Block_byref_a_0
结构体,
指向外部__Block_byref_a_0
的地址,
其中它的__forwarding指针指向自身
一般情况下,对被截获变量进行赋值操作需要添加 __block
修饰符(注意是赋值!!, 赋值≠使用)
NSMutableArray *array = [NSMutableArray array];
void(^Block)(void) = ^{
[array addObject:@123];
}
//不需要添加 __block,因为是使用
当__block修饰外界变量时
int main(){
__block int a = 10;
void(^block)(void) = ^{
printf("Felix %d ", a);
};
block();
return 0;
}
将代码编译成C++源码
// 原代码
__block int a = 10;
// c++源码
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
(void*)0,
(__Block_byref_a_0 *)&a,
0,
sizeof(__Block_byref_a_0),
10
};
可以看到 变量a 变成了 结构体类型__Block_byref_a_0
下面再看看结构体__Block_byref_a_0
的构造
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
通过上面结构体的初始化和结构体的构造, 可以获得以下信息:
- __forwarding存放的是自己本身的地址
- 结构体内的a变量存放的是外部变量a的值
主结构体__main_block_impl_0
的变化
如何从栈指向堆,并建立联系呢?
apple源码,如图:
copy->forwarding = copy; 就是将堆结构体的forwarding指针指向自身 src->forwarding = copy; 就是将栈结构体的forwarding指针指向堆结构体
这样,苹果工程师在背后悄悄地将block copy到了堆上, 而且栈上的block从未被我们利用过。
在看看block入口静态函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a)++;
}
通过当前栈空间主结构体上的__Block_byref_a_0
结构体指针,访问指向堆空间的__forwarding
成员,并获取堆空间上变量的值。
当然,不仅__block修饰的变量会这样,前文的对象类型变量同样会在copy函数内部被转化成类似的结构体进行处理。
__block
修饰的属性在底层会生成响应的结构体,保存原始变量的指针,并传递一个指针地址给block——因此是指针拷贝
__block
所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。
__block修饰的变量成了对象
__forwarding存在意义
不论在任何内存位置,都可以顺利访问同一个__block变量.
Reference
1 深入研究 Block 捕获外部变量和 __block 实现原理
探寻Block的本质(6)—— block的深入分析block的使用场景 大家应该都知道,如果想在block - 掘金 (juejin.cn)