如下代码就是个block,block不会主动调用
^{
NSLog(@"this is a block!");
};
运行之后,发现并没有打印。
如果在block后面加个(),发现block就立马调用了:
^{
NSLog(@"this is a block!");
}();
运行后:
this is a block!
一般我们都把block保存起来,在需要的时候才调用。
一. block的底层结构(block的本质)
block是封装了函数调用以及函数调用环境的OC对象
下面我们验证上面这句话。
写个简单的block,其中block内部使用了block外部的age变量:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 20;
//block定义
void (^block)(int, int) = ^(int a , int b){
NSLog(@"this is a block! -- %d", age);
};
age = 30;
//block调用
block(10, 10);
}
return 0;
}
block调用之后打印:this is a block! -- 20 。为什么不是30?
源码分析
通过“xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件”指令,将上面代码转成C++代码之变成这样:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 20;
//block底层定义
void (*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
//block底层调用
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
}
return 0;
}
但是由于底层的代码添加了许多强转,我们简化代码,如下:
//block底层定义
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
//block底层调用
block->FuncPtr(block, 10, 10);
一共就两行代码,我们就一个一个研究
① block底层定义
//block底层定义
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
先看__main_block_impl_0这个函数,我们发现它被定义在一个同名结构体里面,这个__main_block_impl_0结构体就是block的底层实现
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
// 构造函数(类似于OC的init方法),返回结构体对象
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock; //isa指向类对象,就比如str的isa指向NSString
impl.Flags = flags;
impl.FuncPtr = fp; //外面的__main_block_func_0函数地址传进来,保存在这里
Desc = desc; //外面的__main_block_desc_0结构体地址传进来,保存在这里
}
};
先看结构体里面的__main_block_impl_0函数,其实这个函数在C++中是构造方法,类似于OC的init方法,返回结构体对象。函数的第一个参数是fp指针,第二个参数是desc指针。
接下来再看看__main_block_impl_0结构体,它第一个成员是个结构体:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
发现这个结构体里面第一个成员就是isa,验证了block本质上也是一个OC对象。
第二个成员是指向__main_block_desc_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)};
可以发现,这个结构体被重新命名为__main_block_desc_0_DATA,默认传入了两个值0和sizeof(struct __main_block_impl_0),block的底层就是__main_block_impl_0结构体,所以这个结构体第二个值保存的是block的大小。
接下来我们看一下__main_block_impl_0函数的参数,第一个参数是指向__main_block_func_0函数的指针,如下:
//封装了block执行逻辑的函数
//第一个参数是block,后面是block调用的时候传入的参数
void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_0, age);
}
可以发现,我们写的代码块里面的NSLog被封装成了__main_block_func_0函数,它引用了block外面的成员变量age。
第二个参数就是上面我们说的__main_block_desc_0结构体的地址。
现在我们知道了,首先__main_block_impl_0函数有两个参数,第一个参数是__main_block_func_0函数的地址(这个函数里面封装了我们block里面执行的代码),第二个参数是__main_block_desc_0结构体的地址(这个结构体里面有保存block的大小),整个函数的返回值是个__main_block_impl_0结构体,block底层就是__main_block_impl_0结构体,最后再获取__main_block_impl_0结构体的地址,赋值给左边的“block”变量,然后我们拿到“block”变量就可以做其他事情了,至此,block定义完成。
② block底层调用
//block底层调用
block->FuncPtr(block, 10, 10);
这句代码就很简单了,直接取出block里面的FuncPtr函数,传入参数进行调用。
这里你可能会有个小疑问,不应该是通过“block-> impl->FuncPtr(block, 10, 10)”来拿到FuncPtr吗?
其实我们在简化之前,代码是这样的:
((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
可以发现系统把block强转成__block_impl类型的了,由于impl又是__main_block_impl_0结构体的第一个成员,所以impl的地址和__main_block_impl_0结构体的地址是一样的,强转之后可以直接获取到FuncPtr。
根据如上分析,验证了,block是封装了函数调用以及函数调用环境的OC对象。
总结:
如上图所示,block底层就是一个__main_block_impl_0结构体,它由三个部分组成:
- 第一部分是impl,它是个结构体,里面有isa指针和FuncPtr指针,FuncPtr指针指向__main_block_func_0函数,这个函数里面封装了block需要执行的代码。
- 第二部分是desc,它是个指针,指向__main_block_desc_0结构体,它里面有一个Block_size用来保存block的大小。
- 第三部分是age,它把外面访问的成员变量age封装到自己里面了。
关于block的本质,网上还有一张图,可自己参考:
二. block的变量捕获(capture)
在上面我们留了一个问题,block调用之后打印:this is a block! -- 20 。为什么不是30?
局部变量和全局变量的捕获
- 如果是被auto修饰的局部变量,会被捕获,是值传递
- 如果是被static修饰的局部变量,会被捕获,是指针传递
- 如果是全局变量,不会被捕获,因为可以直接访问
auto自动变量,离开作用域就销毁,默认省略auto。比如我们常见的 int age = 10,其实就是默认省略了auto,本来应该是auto int age = 10
我们执行如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int age = 10;
static int height = 10;
void (^block)(void) = ^{
// age的值捕获进来
// height的指针捕获进来
NSLog(@"age is %d, height is %d", age, height);
};
age = 20;
height = 20;
block();
}
return 0;
}
打印:
age is 10, height is 20
这就解释了上面的疑问,因为age是值捕获,所以修改外面的age值不会影响block里面的age值。
① 源码分析
为了探究block内部是怎么做到的,我们将上面的代码转成C++代码,抽取关键的代码,如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *height;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
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
int *height = __cself->height; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_t9gvrhjs7gv_m4kb_8q3r_980000gn_T_main_12eb7d_mi_1, age, (*height));
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
auto int age = 10;
static int height = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 20;
height = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
可以看出age和height都被捕获了,age是值捕获,height是指针捕获。
- 我们定义block,其实就是初始化__main_block_impl_0结构体,定义block的时候会把age和&height传进去,这个结构体里面有一个age一个height指针用于接收传进去的值,这行代码“age(_age), height(_height)”就是保证外面的变量改变的时候实时改变结构体里面的age和height指针的值。
- 我们调用block的时候,其实就是执行__main_block_func_0函数,这个函数会获取__main_block_impl_0结构体中age和height指针的值,所以打印的时候就会把age和*height的值打印出来。
- 所以执行完block之后age的值没改变,因为是值传递,height的值改变了,因为是指针传递。
② 为什么auto变量是值传递,static变量是指针传递呢?
因为auto变量在{}结束之后就会被销毁,被销毁之后变量的内存就消失了,将来在其他地方执行block的时候就不可能再去访问auto变量的内存了。
但是static变量不一样,被static修饰的变量一直在内存中,只要捕获它的指针就可以随时访问它的内存了。
③ 为什么全局变量不需要捕获呢?
下面我们验证下,如下代码:
int age_ = 10;
static int height_ = 10;
void (^block)(void);
void test()
{
auto int a = 10;
static int b = 10;
block = ^{
NSLog(@"age is %d, height is %d", a, b);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
转成C++文件之后,代码如下:
int age_ = 10;
static int height_ = 10;
void (*block)(void);
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
int a;
int *b;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int *b = __cself->b; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_fd2a14_mi_0, a, (*b));
}
void test()
{
auto int a = 10;
static int b = 10;
block = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, a, &b));
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
test();
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
可以看出age_和height_没被捕获,a和b被捕获了,相信不用解释上面的代码也能看懂。
总结
为什么局部变量需要捕获,全局变量不需要捕获呢?
因为作用域的问题。局部变量的作用域在{}中,当在其他函数中想要访问局部变量的值,肯定要把它捕获啊(auto变量是值捕获,static变量是指针捕获)。对于全局变量,任何地方都可以访问,所以没必要捕获。
④ self的捕获
下面代码,创建MJPerson对象:
MJPerson.h
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
@property (copy, nonatomic) NSString *name;
- (void)test;
- (instancetype)initWithName:(NSString *)name;
@end
MJPerson.m
#import "MJPerson.h"
@implementation MJPerson
- (void)test
{
void (^block)(void) = ^{
NSLog(@"-------%d", [self name]);
};
block();
}
- (instancetype)initWithName:(NSString *)name
{
if (self = [super init]) {
self.name = name;
}
return self;
}
@end
如上代码,在test方法里面访问name属性,那么self会被捕获吗?name会被捕获吗?
将MJPerson.m转成C++代码:
struct __MJPerson__test_block_impl_0 {
struct __block_impl impl;
struct __MJPerson__test_block_desc_0* Desc;
MJPerson *self;
__MJPerson__test_block_impl_0(void *fp, struct __MJPerson__test_block_desc_0 *desc, MJPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __MJPerson__test_block_func_0(struct __MJPerson__test_block_impl_0 *__cself) {
MJPerson *self = __cself->self; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_MJPerson_1027e6_mi_0, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")));
}
static void _I_MJPerson_test(MJPerson * self, SEL _cmd) {
void (*block)(void) = ((void (*)())&__MJPerson__test_block_impl_0((void *)__MJPerson__test_block_func_0, &__MJPerson__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
static instancetype _I_MJPerson_initWithName_(MJPerson * self, SEL _cmd, NSString *name) {
if (self = ((MJPerson *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MJPerson"))}, sel_registerName("init"))) {
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)name);
}
return self;
}
可以看出self被捕获了,并且是指针捕获,既然被捕获,就说明self是局部变量。
为什么self是局部变量呢?
其实每个方法都两个隐式参数,一个是self一个是_cmd,self是方法调用者,_cmd是方法名,既然self被当做参数了,那self肯定是局部变量了,也可以在上面的代码中进行验证,如下:
void _I_MJPerson_test(MJPerson * self, SEL _cmd)
对于[self name],在上面的代码可以看出是给self发送消息,如下:
objc_msgSend((id)self, sel_registerName("name"))
所以,block会捕获self,如果想要访问self中的成员变量就给self发送消息就好了(self都被捕获了,肯定可以获取到self中的其他信息了)。
总结:局部变量会捕获,全局变量不会捕获。
Demo地址:block的本质和变量捕获
三. block的类型
接下来我们讲的都是在MRC环境下。
block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承于NSBlock类型
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
1. 验证block是OC对象
前面我们说了,block是OC对象,既然是OC对象就可以调用class方法查看类型
下面验证block是OC对象,运行代码:
void (^block)(void) = ^{
NSLog(@"Hello");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
打印结果:
__NSGlobalBlock__
__NSGlobalBlock
NSBlock
NSObject
可以看出上面的block继承关系是:__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
上面的block最终继承于NSObject,说明block是OC对象。现在我们也明白了,block内部的isa就是从NSObject中获取的,而且isa指向block的类对象。
2. 三种block在内存中的分布
刚才说了block有三种类型,这三种block在内存中的分布如下图:
- 上图的text区就是代码段,我们编写的代码都在代码段,代码段内存地址比较小,上图从上往下,内存地址越来越大。
- data区就是数据段,数据段一般都放一些全局变量。
- 堆:动态分配内存,需要程序员自己申请内存,也需要程序员自己管理内存。比如[NSObject alloc]或者malloc()创建的对象就是存放在堆,需要我们自己管理内存(只不过ARC不需要你管了)。
- 栈:系统自动分配内存,自动销毁内存。存放局部变量,系统会在{}结束之后销毁局部变量。栈的内存地址最大。
验证内存地址由低到高:
int age = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
NSLog(@"数据段:age %p", &age);
NSLog(@"堆:obj %p", [[NSObject alloc] init]);
NSLog(@"栈:a %p", &a);
NSLog(@"数据段:class %p", [MJPerson class]);
}
return 0;
}
打印:
数据段:age 0x100002488
堆:obj 0x10061b5e0
栈:a 0x7ffeefbff46c
数据段:class 0x100002438
如上,如果想知道MJPerson类对象放在哪里,可以看它的地址和哪个比较接近,比较地址可知,放在数据段。
现在我们知道了三种block在内存中的分布:
__NSGlobalBlock__是放在数据段的,也就是和全局变量放在一起。
__NSMallocBlock__是放在堆区的,和一般的OC对象一样的,需要我们自己管理内存。
__NSStackBlock__是放在栈区的,和局部变量是一样的,系统自动管理内存。
但是什么样的block才是这三种block的某一种类型呢?
先看结论:
block类型 | 环境 |
---|---|
__NSGlobalBlock__ | 没有访问auto变量 |
__NSMallocBlock__ | __NSStackBlock__调用了copy |
__NSStackBlock__ | 访问了auto变量 |
GlobalBlock没有访问auto变量,这种类型的block都可用方法代替,不常用。
StackBlock访问了auto变量,放在栈区,这时候block捕获了auto变量的值,然后存储在block结构体内部,栈区是系统自动管理的,所以在代码块结束之后,block内存会被销毁,这时候block结构体内部的值就是乱七八糟的了,block就会有问题,如下:
void (^block)(void);
void test2()
{
// NSStackBlock
int age = 10;
block = ^{
NSLog(@"block---------%d", age);
};
NSLog(@"%@", [block class]);
}
执行方法:
test2();
block();
打印:
__NSStackBlock__
block---------2634434;
可以看出打印age的值,就是乱的。
那么如何解决这个问题呢?
可以把block从栈放到堆里面,每一种类型的block调用copy后的结果如下所示:
block类型 | 副本源的配置存储域 | 复制效果 |
---|---|---|
__NSGlobalBlock__ | 程序的数据区段 | 什么也不做 |
__NSMallocBlock__ | 堆 | 引用计数器增加 |
__NSStackBlock__ | 栈 | 从栈复制到堆 |
比如,将上面NSStackBlock加个copy变成NSMallocBlock,打印就是正确的,如下:
void test2()
{
// NSStackBlock ->NSMallocBlock
int age = 10;
block = [^{
NSLog(@"block---------%d", age);
} copy];
NSLog(@"%@", [block class]);
[block release]; //如果是MAC,由于放在堆区了,要自己release
}
打印:
__NSMallocBlock__
block---------10
关于NSGlobalBlock的copy操作和NSMallocBlock的copy操作的结果可自行验证。
Demo地址:block的类型