iOS block实现原理
block是什么
- (void)viewDidLoad {
[super viewDidLoad];
void (^block)(void) = ^{};
NSLog(@"%@", block);
id obj = block;
NSLog(@"%@", obj);
}
如上我们写一个简单的block,然后在最后一行打上断点,用xcode查看对象继承链和对象结构,打印如下
可见这个block是一个__NSGlobalBlock__
类型的对象,继承链如下
__NSGlobalBlock__->NSBlock->NSObject
该对象内部通过函数指针实现了代码块的存储,block的执行过程就是函数指针所指向函数的执行过程,因此:
block对象可以调用NSObject中定义的方法和通过分类给NSObject动态添加的方法,其调用过程符合OC消息发送机制和消息转发机制,而block本身的执行过程是通过函数指针配合结构体来实现的,因此不符合OC消息机制
所以当我们进行如下调用的时候
- (void)viewDidLoad {
[super viewDidLoad];
void (^block)(void);
block();
}
运行时会直接crash,指示EXC_BAD_ACCESS
访问坏地址或者野指针
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(block)];
}
而如上调用,运行时会抛出异常Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController block]: unrecognized selector sent to instance 0x11fe0b1f0',指示找不到方法
block的类型
block的类型是NSBlock继承自NSObject,子类型有三个
-
__NSGlobalBlock__
:存储在数据段 -
__NSStackBlock__
:存储在栈上 -
__NSMallocBlock__
:存储在堆上
苹果有意隐藏了NSBlock的实现,我们不能显式的通过如下代码创建block,否则它一定在堆上,类型一定是__NSMallocBlock__
NSBlock *block = [[NSBlock alloc] init];
我们创建block都是通过字面量创建的,因此起初不会有存储在堆上的block,这也是我们在MRC时代需要对栈上的block进行copy的原因,因为超出作用域就自动释放了,对全局block进行copy是没有作用的,就像对存储在数据段的字符串copy一样
因此block的类型是这样区分的
block类型 | 因素 | 内存分配 |
---|---|---|
__NSGlobalBlock__ |
没有访问外部的堆区或者栈区变量 | 数据段 |
__NSStackBlock__ |
访问了外部的堆区或者栈区变量 | 栈区 |
__NSMallocBlock__ |
栈区block的拷贝 | 堆区 |
苹果为什么这么设计就很好理解了吧
- 全局block
没有外部依赖,创建后不销毁,访问速度快又没有反复创建的开销 - 栈区block
有外部依赖,不参与外部变量的内存管理,分配在栈上,作用域结束自动销毁 - 堆区block
有外部依赖,要参与外部变量的内存管理,分配在堆上,需要程序员手动管理内存,不用时清空引用计数,自动销毁
当然!!!
ARC帮我们做了很多事情,ARC下分配在栈上的block会自动进行copy,因此ARC下只有两种类型的block,全局block和堆区block,那么苹果为什么要这么做呢,因为栈上面的block经常被忘记copy,但是却经常在超过作用域还在访问,就会直接EXC_BAD_ACCESS
,因此苹果在ARC下直接做了这件事
block捕获变量
因此当我们讨论block对外部变量的捕获情况,我们只需要关心ARC下的堆区block即可,笔者将其总结成一个表格,如下
外部变量类型 | 捕获情况 | 访问方式 |
---|---|---|
全局变量 | 否 | 数据段地址访问 |
静态变量 | 否 | 数据段地址访问 |
基础类型局部变量 | 是 | 拷贝,栈地址访问 |
__block基础类型局部变量 | 是 | 引用,栈地址访问 |
对象类型(缺省关键字strong) | 是 | 引用计数+1,堆地址访问 |
__strong对象类型 | 是 | 引用计数+1,堆地址访问 |
__weak对象类型 | 是 | 堆地址访问 |
__block对象类型 | 是 | 引用计数+1,堆地址访问 |
block内存管理
这里想聊一下几个修饰block的内存关键字,当我们讨论内存管理,我们只需讨论block访问外部堆区、栈区变量的情况,因为数据段内存不会回收直到进程退出,且全局皆可访问,所以block无需对其捕获
栈上的block不会对外部对象产生强引用,不论其关键字是__strong还是__weak
堆上的block内部会根据捕获的外部对象的访问关键字__strong、__weak或__unsafe_unretained对其进行强、弱引用
__block修饰的变量会被包装成结构体,基础类型的变量结构体内包装的是其引用,对象类型的变量结构体内包装的是指针的引用,相当于二级指针,因此被__block修饰的变量能在block内被修改
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf doSomething];
};
上面代码__weak避免循环引用,__strong保证block异步执行时self还在,block执行完毕后strongSelf指针被回收,self所指对象引用计数-1