目录
一、block是什么
二、block的本质
三、block的类型
一、block是什么
简单地说,block跟Swift和Dart里的闭包(匿名函数)差不多,我们都知道在Swift和Dart里函数是一等公民,所以函数可以赋值给变量、也可以作为函数的参数、也可以作为函数的返回值,也就是说一个函数可以传来传去。但是在OC里函数不是一等公民,它不能传来传去,不过OC又提供了一种特殊的对象那就是block,它就像一个匿名函数一样可以可以赋值给变量、也可以作为函数的参数、也可以作为函数的返回值,但是要注意它本质上不是一个函数而是一个OC对象。
本质地说,block其实也是一个OC对象,所以它的内存里存储的是什么和普通OC对象差不多,只不过普通OC的对象用来包装一些普通的数据(如字符串数据、数组数据、字典数据等),而block则用来包装一段代码以及这段代码的调用环境。所谓包装一段代码,是指block内部会把block的参数、返回值、执行体封装成一个函数,并且存储该函数的内存地址;所谓包装这段代码的调用环境,是指block内部会捕获变量,并且存储这些捕获的变量。(这段话会在下一小节详细证明)
- block的声明
block作为属性时,这样声明:
// block的类型为:int (^)(int a, int b)
// block的名字为:block
@property (nonatomic, copy) int (^block)(int a, int b);
block作为方法的参数时,这样声明:
// block的类型为:void (^)(NSData *data, NSError *error)
// block的名字为:completionHandler
- (void)fecthDataWithCompletionHandler:(void (^)(NSData *data, NSError *error))completionHandler;
如果项目中使用了大量相同类型的block,那为了使代码更简洁,我们可以先typedef
一下block的类型,然后再声明。则上面两例可以写成这样:
typedef int (^Block)(int a, int b);
@property (nonatomic, copy) Block block;
typedef void (^Block)(NSData *data, NSError *error);
- (void)fecthDataWithCompletionHandler:(Block)completionHandler;
- block的实现
箭头打头就代表block的实现,如果block没有返回值,可省略returnType,如果block没有参数,可省略params。
^returnType(params) {
// block的执行体
};
不过通常我们都会把block的实现用一个变量记录下来,以便将来调用,就像函数那样。
returnType (^blockName)(params) = ^returnType(params) {
// block的执行体
};
- block的调用
像C语言那样加小括号就代表block的调用,如果block没有返回值,则不接收返回值,如果block没有参数,可省略params。
returnType v = blockName(params);
- block代码的执行顺序
block代码的执行顺序永远都是:block的声明 --> block的实现 --> block的调用 --> 最后返回去真正去执行block的实现代码。举例如下:
#import "ViewController.h"
@interface ViewController ()
// 第一步:block的声明
@property (nonatomic, copy) int (^block)(int a, int b);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 第二步:block的实现
self.block = ^int(int a, int b) {
// 第四步:最后返回去真正去执行block的实现代码
return a + b;
};
// 第三步:block的调用
int num = self.block(1, 2);
NSLog(@"%d", num);
}
@end
二、block的本质
我们简单创建一个block,并调用它。
// 创建一个block
void (^block)(void) = ^{
NSLog(@"11");
};
// 调用block
block();
接着用clang
编译器把这段OC代码转换成C/C++代码,来窥探一下block的本质。(伪代码)
首先编译器会把block转换成它的本质——即一个C++的__block_impl_0
结构体,该结构体内部有两个成员变量,第一个成员变量内部有一个isa
指针指向block所属的类,这也证明我们上面所说的“block其实也是一个OC对象”,还有一个FuncPtr
指针指向该block对应的函数;第二个成员变量内部则存储着该block的一些描述信息,如该block的实际大小、copy
函数、dispose
函数等。此外,该结构体内部还有一个block构造函数,它用来创建并初始化一个block——即一个__block_impl_0
类型的结构体。当然我们也不能忘了,block结构体内部还可以有更多的成员变量,它们就是block捕获并存储的变量,也就是我们上面所说的“包装代码的调用环境”。
// block的本质,是一个C++结构体
struct __block_impl_0 {
// 第一个成员变量
struct __block_impl impl;
// 第二个成员变量(这个不用太关心)
struct __block_desc_0* Desc;
/**
* block的构造函数
*
* @param fp block对应函数的内存地址
* @param desc block描述信息结构体的内存地址
*
* @return 返回一个当前类型的结构体————即返回一个__block_impl_0类型的结构体
*/
__block_impl_0(void *fp, struct __block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock; // block所属的类
impl.Flags = flags;
impl.FuncPtr = fp; // 把block对应函数的内存地址存储在block内部
Desc = desc; // 把block描述信息结构体的内存地址存储在block内部
}
};
// block实现信息结构体
struct __block_impl {
void *isa; // 指向block所属的类
int Flags;
int Reserved;
void *FuncPtr; // 指向block对应的函数
};
// block描述信息结构体
struct __block_desc_0 {
size_t reserved; // 预留
size_t Block_size; // block的实际大小
};
然后编译器会把block的参数、返回值、执行体封装成一个__block_func_0
函数,也就是我们上面所说的“包装一段代码”,将来创建block的时候,block内部的FuncPtr
指针就会指向这个函数,并把block的描述信息封装进一个__block_desc_0
结构体(这个不用太关心)。
// block对应的函数
void __block_func_0(struct __block_impl_0 *__cself) {
NSLog("11");
}
// block对应的描述信息
struct __block_desc_0 {
size_t reserved;
size_t Block_size; // block的实际大小
} __block_desc_0_DATA = {
0,
sizeof(struct __block_impl_0) // 计算block的实际大小
};
知道了这些,上面那几行OC代码其实就对应着下面这几行C/C++代码,创建一个block的本质就是调用block的构造方法创建一个结构体,构造方法会接收block对应的函数的地址和block对应的描述信息的地址作为参数,接收后会存储在它内部,然后把这个结构体的内存地址赋值给*block
这里的block
指针变量;而调用block的本质就是找到block内部FuncPtr
指针指向的函数来调用。
// 创建一个block
void (*block)(void) = &__block_impl_0(
__block_func_0,// 把函数的地址传进去
&__block_desc_0_DATA // 把结构体的地址传进去
);
// 调用block
block->impl.FuncPtr(block);
三、block的类型
1、全局block、栈block、堆block
(注意:这一小节的示例代码都是在MRC下的)
block有三种类型:全局block(__NSGlobalBlock__
)、栈block(__NSStackBlock__
)、堆block(__NSMallocBlock__
),它们都继承自NSBlock
,并最终继承自NSObject
。那什么是全局block?什么是栈block?什么又是堆block?全局block是指存储在全局区的block,栈block是指存储在栈区的block,堆block是指存储在堆区的block,所以说看一个block是什么类型,不是看它在代码的什么位置定义的,而是看它存储在哪块内存分区中——即系统把它存储在哪块内存分区中了。这在代码中有什么体现呢?也就是说我们如何通过代码一眼就能知道这个block是什么类型的呢?
- 全局block
没有访问外界普通局部变量的block就是全局block,系统会把这样的block放在全局区。
// 普通全局变量
//int age = 25;
// 静态全局变量
//static int age = 25;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 静态局部变量
static int age = 25;
void (^block)(void) = ^{
// 访问外界的变量
NSLog(@"%d", age);
};
NSLog(@"%@", [block class]); // __NSGlobalBlock__
NSLog(@"%@", [[block class] superclass]); // __NSGlobalBlock
NSLog(@"%@", [[[block class] superclass] superclass]); // NSBlock
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]); // NSObject
}
return 0;
}
- 栈block
访问了外界普通局部变量的block就是栈block,系统会把这样的block放在栈区,可见栈block和全局block是完全对立的。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 普通局部变量
int age = 25;
void (^block)(void) = ^{
// 访问外界的变量
NSLog(@"%d", age);
};
NSLog(@"%@", [block class]); // __NSStackBlock__
NSLog(@"%@", [[block class] superclass]); // __NSStackBlock
NSLog(@"%@", [[[block class] superclass] superclass]); // NSBlock
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]); // NSObject
}
return 0;
}
- 堆block
对栈block执行一下copy
操作,copy
方法返回的就是一个堆block,所以说堆block就是把栈block copy
了一份到堆区。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 普通局部变量
int age = 25;
void (^block)(void) = [^{
// 访问外界的变量
NSLog(@"%d", age);
} copy]; // 需适时的[block release]一下
NSLog(@"%@", [block class]); // __NSMallocBlock__
NSLog(@"%@", [[block class] superclass]); // __NSMallocBlock
NSLog(@"%@", [[[block class] superclass] superclass]); // NSBlock
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]); // NSObject
}
return 0;
}
你会发现在平常的开发中,我们用到的总是堆block,而不是栈block。这是因为在ARC下我们使用block的时候,系统很多情况下都会自动帮我们复制一份栈block到堆区,而MRC下则需要我们手动调用copy
方法让系统复制一份栈block到堆区。那为什么非要复制一份栈block到堆区?栈block有什么问题吗?
void (^block)(void);
void test() {
// 普通局部变量
int age = 25;
block = ^{
// 访问外界的变量
NSLog(@"%d", age); // -272632440,不是25
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
试看上面这段代码,我们定义了一个block,并且它的实现部分访问了外界的普通局部变量,所以它是一个栈block。而我们知道栈内存是由系统自己管理的,在出了相应的作用域后栈内存就会自动释放,可以供别人使用了,你的数据有可能就被别人替换掉。那么test
函数执行完后,block超出作用域,已经被系统释放掉了,此时虽然说我们还能通过block这个全局变量去访问那块内存,但那块内存里存的很有可能已经是别人的数据了,所以block()
这个调用本身其实已经没有意义了,可以根据block的执行顺序去分析一下这段代码。
总结一下:为什么要把栈block到
copy
到堆区?block刚被创建出来时,若不是全局block就是栈block,而栈内存又是系统自动管理的,一旦超出变量的作用域,变量对应的内存就会被释放,所以如果不把栈block复制到堆区,就很有可能我们在调用栈block的时候它已经被销毁了,也就是说block内存里的数据已经都是垃圾数据了,即便能调用成功那也是毫无意义地调用,会导致数据错乱。
额外的考虑,拿来玩儿
上面我们知道了对栈block执行copy
操作是在堆区复制出了一个新的block,那对全局block和堆block执行copy
操作呢?
- 全局block执行
copy
操作
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 静态局部变量
static int age = 25;
void (^block)(void) = ^{
// 访问外界的变量
NSLog(@"%d", age);
};
NSLog(@"%p", block); // 0x100001060
NSLog(@"%p", [block copy]); // 0x100001060
}
return 0;
}
- 堆block执行
copy
操作
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 普通局部变量
int age = 25;
void (^block)(void) = [^{
// 访问外界的变量
NSLog(@"%d", age);
} copy];
NSLog(@"%p", block); // 0x102005650
NSLog(@"%p", [block copy]); // 0x102005650
}
return 0;
}
可见:
block类型 | 执行copy 操作后的效果 |
---|---|
全局block | 什么也不做,不会产生新的block,旧block的引用计数也不会加1,因为内存是放在全局区的,生命周期跟App同步,你用就行了,不必增加引用计数 |
栈block | 复制一份栈block到堆区 |
堆block | 仅仅是block的引用计数加1,不会产生新的block |
2、ARC下系统会在某些情况下自动copy
一份栈block到堆区
(注意:这一小节的示例代码都是在ARC下的)
-
block赋值给一个强指针时(即
__strong
修饰的指针),系统会自动把该栈block复制到堆区,可以理解为Swift和Dart里函数复制给变量时
typedef void(^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 25;
Block block = ^{
NSLog(@"%d", age);
};
// 等价于
// __strong Block block = ^{
//
// NSLog(@"%d", age);
// };
NSLog(@"%@", [block class]); // __NSMallocBlock__,是一个堆block
}
return 0;
}
上面代码中block变量是个强指针,等号右边的block本来是个栈block,但是在赋值给强指针时系统会自动把该栈block复制到堆区,所以就返回了一个堆block。当然如果我们把block变量变成弱指针,那block自然就还是栈block,系统不会自动复制了。
typedef void(^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 25;
__weak Block block = ^{
NSLog(@"%d", age);
};
NSLog(@"%@", [block class]);// __NSStackBlock__,是一个栈block
}
return 0;
}
- block作为函数的参数时,系统会自动把该栈block复制到堆区,可以理解为Swift和Dart里函数作为函数的参数时
typedef void(^Block)(void);
void test(Block block) {
NSLog(@"%@", [block class]); // __NSMallocBlock__,是一个堆block
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 25;
Block block = ^{
NSLog(@"%d", age);
};
test(block);
}
return 0;
}
- block作为函数的返回值时,系统会自动把该栈block复制到堆区,可以理解为Swift和Dart里函数作为函数的返回值时
typedef void(^Block)(void);
Block test() {
int age = 25;
Block block = ^{
NSLog(@"%d", age);
};
return block;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%@", [test() class]);// __NSMallocBlock__,是一个堆block
}
return 0;
}
上面代码中test
函数返回的block本来是个栈block,但是在作为函数的返回值系统会自动把该栈block复制到堆区,所以调用test
函数时就得到了一个堆block。
- GCD方法里的block,系统都会自动复制到堆区
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
// 等等......
- Foundation框架usingBlock方法里的block,系统都会自动复制到堆区
[[NSArray alloc] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
[[[NSDictionary alloc] init] enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
}];
// 等等......