
一、block是什么
二、block的本质
三、block的类型
一、block是什么
block其实也是一个OC对象,它享有所有OC对象的待遇,只不过普通OC对象用来封装数据,而block用来封装函数以及函数的调用环境。所谓封装函数,是指block内部会把block的参数、返回值、执行体封装成一个函数,并且存储该函数的内存地址;所谓封装函数的调用环境,是指block内部会捕获变量,并且存储这些捕获的变量。
- block的声明
block作为属性时,这样声明:
@property (nonatomic, copy) int (^block)(int a, int b);
block作为方法的参数时,这样声明:
- (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++结构体
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的本质就是一个C++的__block_impl_0
结构体,该结构体内部有两个成员变量,第一个成员变量内部有一个isa
指针指向block所属的类(这也证明block确实是一个OC对象),还有一个FuncPtr
指针指向该block对应的函数;第二个成员变量内部则存储着该block的一些描述信息,如该block的实际大小、copy
函数、dispose
函数等。此外,该结构体内部还有一个block构造函数,它用来创建并初始化一个block——即一个__block_impl_0
类型的结构体。当然我们也不能忘了,block内部还可以有更多的成员变量,它们就是block捕获的变量。
// 首先把block的参数、返回值、执行体封装成一个__block_func_0函数
void __block_func_0(struct __block_impl_0 *__cself) {
// block的执行体
NSLog("11");
}
// 然后把block的描述信息封装进一个__block_desc_0结构体
struct __block_desc_0 {
size_t reserved;
size_t Block_size;// block的实际大小
} __block_desc_0_DATA = {
0,
sizeof(struct __block_impl_0)// 计算block的实际大小
};
// 创建一个block
void (*block)(void) = &__block_impl_0(// &是指获得函数的地址,来调用
__block_func_0,// 把函数的地址传进去
&__block_desc_0_DATA// 把结构体的地址传进去
);
// 调用block
block->impl.FuncPtr(block);
可见创建block的本质,确实就是把block的参数、返回值、执行体封装成一个__block_func_0
函数,并且存储该函数的内存地址,然后block还会捕获变量,并且存储这些捕获的变量。
而调用block的本质,也确实就是找到block内部FuncPtr
指针指向的函数来调用。
三、block的类型
1、全局block、栈block、堆block,堆block就是把栈block copy
了一份到堆区
(注意:这一小节的示例代码都是在MRC下的)
block有三种类型:全局block(__NSGlobalBlock__
)、栈block(__NSGlobalBlock__
)、堆block(__NSGlobalBlock__
),它们都继承自NSBlock。那什么是全局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到堆区,而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到
copy
到堆区?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 | 复制一份栈block到堆区 |
堆block | 仅仅是block的引用计数加1,不会产生新的block |
2、ARC下系统会在某些情况下自动copy
一份栈block到堆区
(注意:这一小节的示例代码都是在ARC下的)
-
block赋值给一个强指针时(即
__strong
修饰的指针),系统会自动把该栈block复制到堆区
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复制到堆区
typedef void(^Block)(void);
Block test() {
int age = 25;
return ^{
NSLog(@"%d", age);
};
}
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) {
}];
// 等等......
网友评论