block是我们开发经常遇到的一个结构,本篇我们就来探索一下它的结构。
block的分类
block的分类相信大家已经很清楚了,分为全局block、堆block和栈block。我们来个例子看看它们的区别
全局block
1
2
3
4
5void (^block)(void) = ^{
};
NSLog(@"%@",block);
///打印结果
<__NSGlobalBlock__: 0x1072d4100>全局block是指不捕获任何外部变量的block,只会使用静态变量和全局变量,存储于内存的
全局区
。堆block
1
2
3
4
5
6
7int a = 10;
void (^block)(void) = ^{
NSLog(@"Cooci - %d",a);
};
NSLog(@"%@",block);
///打印结果
<__NSMallocBlock__: 0x60000130c4b0>堆block会捕获外部变量,存储于内存的
堆区
。栈block
1
2
3
4
5
6
7int a = 10;
void (^__weak block)(void) = ^{
NSLog(@"Cooci - %d",a);
};
NSLog(@"%@",block);
///打印结果
<__NSStackBlock__: 0x7ffee06ac4d8>栈block也会捕获外部变量,和堆block的区别是需要加
__weak
修饰,它存储于内存的栈区
block的循环引用
block引起循环引用的原因
A、B相互持有,所以导致A无法调用dealloc方法给B发送release信号,而B也无法接收到release信号。所以A、B此时都无法释放。如图所示
解决循环引用
我们看一段循环引用的代码
1 | NSString *name = @"JS"; |
这段代码出现了循环引用,因为在block
内部使用了外部变量name
,导致block持有了self
,而self原本是持有block
的,所以导致了self和block的相互持有
。
解决方法:
__weak和__strong组合使用
1
2
3
4
5
6
7
8
9
10typedef void(^JSBlock)(void);
@property(nonatomic, copy) JSBlock jslBlock;
__weak typeof(self) weakSelf = self;
self.jslBlock = ^(void){
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"%@",weakSelf.name);
}
self.jslBlock();这是我们最容易想到的方式,使用__weak打破强引用,__strong的作用的方式self提前释放,而block执行的时候因为self已经释放而拿不到值。
__block定义一个临时变量指向self。
1
2
3
4
5
6
7
8__block ViewController *vc = self;
self.jslBlock = ^(void){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
vc = nil;//需手动释放
});
};
self.jslBlock();这种方式是在方法外部定义一个指向self的变量,block内部捕获临时变量,使用结束后将临时变量置为nil,加__block的原因是需要在block内存对其进行置空操作。
block加一个参数,使用参数
1
2
3
4
5
6
7
8typedef void(^JSBlock)(ViewController *);
@property(nonatomic, copy) JSBlock jslBlock;
self.jslBlock = ^(ViewController *vc){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
});
};
self.jslBlock(self);
Block的底层分析
我们主要通过clang和断点调试的方式分析。
xcrun编译分析
我们首先自定义一个block.c
文件
1 |
|
使用xcrun
命令讲block.c
编译成block.cpp
。xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
我们把类型强转的代码去掉:
1 | int main(){ |
简化之后我们看到block
代码块是一个__main_block_impl_0
,__main_block_impl_0
的结构是一个结构体,它的impl
也是一个结构体
1 | struct __main_block_impl_0 { |
注意:在结构体内部捕获到了外部变量a
,且在结构体内部生成了一个成员变量a
与其对应。
我们对代码做一下修改:
1 | int main(){ |
重新xcrun
一下看看结果:
1 | struct __main_block_impl_0 { |
可以看到,现在的
a
和之前的a
不同的是加了__Block_byref_a_0 *
修饰,这样就可以对捕获到的变量进行修改,传给block是a的地址,所以block内部可以修改。impl.isa = &_NSConcreteStackBlock
说明现在栈类型
根据我们前面的分析这里应该是堆block
,为什么不同呢。fp是一个函数式保存,如果不调用不会执行。
源码分析
我们首先用汇编,查看源码在哪个库中,我们打断点
然后看汇编代码,
我们添加符号断点objc_retainBlock
:
所以我们去libobjc
去搜索objc_retainBlock
1 | id objc_retainBlock(id x) { |
实际调用了_Block_copy
,在libobjc
库中并没有找到方法的实现,我们继续打符号断点:
_Block_copy
函数的实现在libsystem
库中,这个库没有开源,我们找一个替换的库libclosure
的源码分析。我们在libclosure
源码中搜索_Block_copy
:
1 | // Copy, or bump refcount, of a block. If really copying, call the copy helper if present. |
其中Block_layout
结构体的结构是:
1 | struct Block_layout { |
对结构有个简单了解之后,我们打符号断点,看运行中block
的结构。
objc_retainBlock
发现此时的block类型还是
StackBlock
。在
_Block_copy
最后打一个断点:此时
block
的类型就是__NSMallocBlock__
类型了。
blockLayout结构
1 | struct Block_layout { |
我们看Block_descriptor_1
的结构,发现并没有上面调试打印的signature
信息。
我们看源码发现:
1 |
|
Block_descriptor_2
和Block_descriptor_3
是可选的,它们是通过Block_descriptor_1
内存平移得到的。
捕获变量的copy
_Block_copy
1 | // Copy, or bump refcount, of a block. If really copying, call the copy helper if present. |
_Block_copy
主要是将block从栈区拷贝到堆区
- 如果需要释放,则直接释放
- 如果是
globalBlock
不需要copy,返回 - 剩下两种情况:堆区block和栈区block。由于堆区block需要申请内存,这里到这里只能是栈block。
- 通过
malloc
申请内存空间用于接收block - 通过remove将block拷贝至新申请的内存中
- 设置block对象的类型为堆区block。将
isa
指向__NSConcreteMallocBlock
- 通过
_Block_object_assign
先看一个枚举的定义:
1 | // Block 捕获的外界变量的种类 |
其中用的最多的是BLOCK_FIELD_IS_OBJECT
和BLOCK_FIELD_IS_BYREF
。
1 | static struct Block_byref *_Block_byref_copy(const void *arg) { |
_Block_object_assign
是在底层编译代码中,外部变量拷贝时调用的方法就是它。
- 如果是普通对象,交给系统arc处理,拷贝对象指针,引用技术+1,外界变量不能释放。
- 如果是block类型的变量,通过_Block_copy操作,将block从栈区拷贝到堆区。
- 如果是
__block
修饰的变量,调用_Block_byref_copy
函数,进行内存拷贝以及常规处理。
_Block_byref_copy
1 | static struct Block_byref *_Block_byref_copy(const void *arg) { |
- 将传入的对象,强转为
Block_byref
结构体类型对象。保存 - 如果没有将变量拷贝到堆上,就申请内存进行拷贝
- 如果已经拷贝,则进行处理并返回
- 其中copy和src的forwarding指针都是指向同一片内存。这就是为什么
__block
修饰的对象具有修改的能力。
三层copy小结
- 第一层:通过
_Block_copy
实现对象的自身拷贝
,从栈区拷贝至堆区 - 第二层:通过
_Block_byref_copy
方法,将对象拷贝为Block_byref
结构体类型 - 第三次:调用
_Block_object_assign
方法,对__block
修饰的当前变量的拷贝