本文主要讲述以下几个问题:
- 什么是block
- block有多少种类型
- block如何捕获外部变量
- 循环引用如何解决
- block copy 的不同结果
__block修饰的变量在底层是什么样的
一、 block是什么
查看c++代码
在 xcode 中生成两个文件:TestBlock.h 、TestBlock.m 。其中 TestBlock.m 添加如下代码:
-(void)blockFunc {
int n = 10;
int (^myBlock)(int m) = ^(int m){
return n + m ;
};
NSLog(@"%d",myBlock(1));
}
然后通过 clang -rewrite-objc TestBlock.m 命令将 TestBlock.m 转换出 TestBlock.cpp 文件,该文件中是底层如何实现 block 的c++代码。
我们看到,blockFunc 方法在底层代码中变成了 _I_TestBlock_blockFunc 函数:
static void _I_TestBlock_blockFunc(TestBlock * self, SEL _cmd) {
int n = 10;
//定义 myBlock
int (*myBlock)(int m) = ((int (*)(int))&__TestBlock__blockFunc_block_impl_0((void *)__TestBlock__blockFunc_block_func_0, &__TestBlock__blockFunc_block_desc_0_DATA, n));
//调用 myBlock
((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);
}
再看 __TestBlock__blockFunc_block_impl_0 的定义:
struct __TestBlock__blockFunc_block_impl_0 {
struct __block_impl impl; //block 主体
struct __TestBlock__blockFunc_block_desc_0* Desc; //block描述
int n; //block 捕获的外部变量 n
//构造函数
__TestBlock__blockFunc_block_impl_0(void *fp, struct __TestBlock__blockFunc_block_desc_0 *desc, int _n, int flags=0) : n(_n) {
impl.isa = &_NSConcreteStackBlock; //初始化为栈 block
impl.Flags = flags;
impl.FuncPtr = fp; //block实现的函数地址
Desc = desc;
}
};
block的实现部分被封装在 __TestBlock__blockFunc_block_func_0 函数中,通过 __TestBlock__blockFunc_block_impl_0 构造函数传递到 __block_impl 结构体中:
static int __TestBlock__blockFunc_block_func_0(struct __TestBlock__blockFunc_block_impl_0 *__cself, int m) {
int n = __cself->n; // bound by copy
return n + m ;
}
struct __block_impl {
void *isa; //isa指针表示block的类型: _NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
int Flags;
int Reserved;
void *FuncPtr; //函数指针,__TestBlock__blockFunc_block_func_0 函数
};
在上述代码中,我们发现,myBlock 实质上是 __TestBlock__blockFunc_block_impl_0 结构体的对象。
总结
block 是在底层是一个结构体对象,它封装了 block 的实现代码和捕获的外部变量。
二、block的三种类型
全局block
NSGlobalBlock 类型,位于全局区。
当在 block 内部不使用外部变量,或只是用静态变量和全局变量时,则是全局 block。
示例代码:
static int b = 100;
void (^Block)(void) = ^{
NSLog(@"== %d",b);
};
NSLog(@"%@",Block);
输出结果为:<__NSGlobalBlock__: 0x10eaca050>
栈block
NSStackBlock 类型,位于栈区。
当使用 __weak 修饰 block 时,被弱引用的 block 没有被 copy 到堆上,则为栈区block。
示例代码:
NSString *str = @"haha";
void (^__weak Block)(void) = ^{
NSLog(@"-- %@",str);
};
NSLog(@"%@",Block);
输出结果为:<__NSStackBlock__: 0x7ffee1136168>
堆block
NSMallocBlock 类型,位于堆区。
当 block 内部引用了外部变量,且block被强引用时,则为堆区block
示例代码:
NSString *str = @"haha";
void (^Block)(void) = ^{
NSLog(@"-- %@",str);
};
NSLog(@"%@",Block);
输出结果为:<__NSMallocBlock__: 0x60000227ef10>
理解不同类型的 block
示例代码:
-(void)testBlock{
NSString *str = @"hello"; //对象类型的 str 局部变量
int num = 10; //值类型的 num 局部变量
void (^__weak weakBlock)(void) ; //声明 weakBlock
//代码块
{
void (^__weak myBlock)(void) = ^{
NSLog(@"num : %d",num);
NSLog(@"str : %@",str);
};
NSLog(@"myBlock : %@",myBlock);
weakBlock = myBlock;
}
NSLog(@"weakBlock : %@",weakBlock);
weakBlock();
}
输出结果:
myBlock : <__NSStackBlock__: 0x7ffeee7b2170>
weakBlock : <__NSStackBlock__: 0x7ffeee7b2170>
num : 10
str : (null)
通过代码与结果分析,我们发现 myBlock 和 weakBlock 都是栈block,并且都指向同一块内存空间 0x7ffeee7b2170 ,而这块内存的生命周期是在 testBlock 函数的作用域之内,即使 myBlock 出了代码块范围,也依然存在于该函数的栈空间中,因此调用 weakBlock() 依然有效。
示例代码:
-(void)testBlock{
int num = 10; //值类型的 num 局部变量
void (^__weak weakBlock)(void) ; //声明 weakBlock
//代码块
{
void (^myBlock)(void) = ^{
NSLog(@"num : %d",num);
};
NSLog(@"myBlock : %@",myBlock);
weakBlock = myBlock;
}
NSLog(@"weakBlock : %@",weakBlock);
weakBlock();
}
输出结果:
myBlock : <__NSMallocBlock__: 0x6000034e55f0>
weakBlock : (null)
程序崩溃:EXC_BAD_ACCESS (code=1, address=0x10)
myBlock 去掉__weak修饰后,变成堆 block,而它的生命周期只在代码块中,出了代码块就被释放掉了,因此再调用weakBlock() 程序崩溃。
示例代码:
-(void)testBlock{
void (^__weak weakBlock)(void) ; //声明 weakBlock
//代码块
{
void (^myBlock)(void) = ^{
NSLog(@"hello~");
};
NSLog(@"myBlock : %@",myBlock);
weakBlock = myBlock;
}
NSLog(@"weakBlock : %@",weakBlock);
weakBlock();
}
输出结果:
myBlock : <__NSGlobalBlock__: 0x10e4790d0>
weakBlock : <__NSGlobalBlock__: 0x10e4790d0>
hello~
不使用外部变量后,myBlock 变成全局 block,在内存中类似于单例,一直存在直到程序终止,因此调用 weakBlock()有效。
三、捕获外部变量
在实践中发现,block 对不同类型和不同作用域的外部变量捕获方式是不尽相同的,总结起来如下列表格:
| 变量类型 | 是否捕获 | 捕获内容 |
|---|
| 局部变量 | 是 | 值类型:捕获值 ; 对象类型: 捕获变量地址和修饰符 |
| 静态局部变量 | 是 | 捕获变量地址 |
| 全局变量/静态全局变量 | 否 | 不捕获,直接访问 |
四、解决 block 循环引用的几个方法
循环引用的问题在 iOS 经常遇到:A对象引用了B对象,B对象中又引用了A对象,导致两个对象都无法释放。
循环引用经常发生在 block 中,下面我们来看个例子:
@interface TestObject()
@property(nonatomic,strong) void(^myBlock)(void);
@end
@implementation TestObject
-(void)blockFunc{
self.myBlock = ^{
NSLog(@"self : %@",self);
};
self.myBlock();
}
在上述代码中,self 持有了 myBlock ,而在 myBlock 中又捕获了 self,因而构成循环引用,下面我们再来看看解决办法。
1、__weak 和 __strong
常用手法,不再赘述。
2、__block
使用 __block 将 block 中的 self 置为nil,从而使得 block 不再持有 self 。
-(void)blockFunc{
__block TestObject *temp = self;
self.myBlock = ^{
NSLog(@"self:%@",temp);
temp = nil;
};
self.myBlock();
}
3、将强引用对象写成 block 的入参
block 的入参不需要被捕获,因此通过参数传入可以使得 block 不对 self 进行持有。
//修改 myBlock 定义,使其可以传入参数
@property(nonatomic,strong) void(^myBlock)(TestObject *obj);
-(void)blockFunc{
self.myBlock = ^(TestObject *obj){
NSLog(@"self:%@",obj);
};
self.myBlock(self);
}
五、block copy
copy操作对于不同类型 block 的结果是不一样的,总结起来看表格:
| block类型 | copy结果 |
|---|
| NSGlobalBlock | copy无效,结果还是同一个block,不会增加引用计数 |
| NSStackBlock | copy 之后变成 NSMallocBlock,得到新内存,类似深拷贝 |
| NSMallocBlock | copy之后还是 NSMallocBlock,但是同一块内存,会增加引用计数,类似浅拷贝 |
在 MRC 模式下,以下这段代码将生成一个 NSStackBlock,然后通过 copy 变成 NSMallocBlock
int n = 0;
void (^block)(void) = ^{
NSLog(@"n = %d",n);
};
NSLog(@"b : %@",block);
block = [block copy];
NSLog(@"bb : %@",block);
在 MRC 模式下输出为:
b : <__NSStackBlock__: 0x7ffee701c158>
bb : <__NSMallocBlock__: 0x60000178b000>
在 ARC模式下输出为:
b : <__NSMallocBlock__: 0x600003854420>
bb : <__NSMallocBlock__: 0x600003854420>
通过上面的比较,我们发现,block 创建时是 NSStackBlock ,而在 ARC 模式下,block创建时就执行了copy操作变成了 NSMallocBlock,并且 NSMallocBlock copy 之后还是同一块内存。
六、__block修饰的变量
__block修饰的变量在底层是一个结构体,我们来看一下代码:
__block int number = 100;
void (^myBlock)(void) = ^{
number = 101;
};
number = 2;
通过 clang rewrite 命令转换为 c++代码:
// __block number 在底层是一个名为 __Block_byref_number_0 的结构体
__attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 100};
// myBlock 定义
void (*myBlock)(void) = ((void (*)())&__TestBlock__blockFunc_block_impl_0((void *)__TestBlock__blockFunc_block_func_0, &__TestBlock__blockFunc_block_desc_0_DATA, (__Block_byref_number_0 *)&number, 570425344));
//block外部修改number的值为 2
(number.__forwarding->number) = 2;
__Block_byref_number_0 结构体:
struct __Block_byref_number_0 {
void *__isa;
__Block_byref_number_0 *__forwarding;
int __flags;
int __size;
int number;
};
从上述代码中,我们可以看到,在block内部和外部,对 number 的赋值都是通过对 number.__forwarding->number 赋值完成的,那么我们来了解一下其缘由。
__forwarding 指针
还是使用上面 __block int number = 100; 的例子,__forwarding指针是 __Block_byref_number_0 结构体的成员,在栈block中,它指向了__Block_byref_number_0 结构体自身(如图1),当栈block 被 copy 变成堆block时,栈区block中的 __forwarding指针指向了堆区block 的__Block_byref_number_0 结构体,而堆区block 的__forwarding指针还是指向结构体自身(如图2)。
图1:
图2:

通过上述代码,我们看到对 number 复制或访问都是通过__forwarding 指针完成的,这样做的好处是无论是栈block还是堆block,通过__forwarding 指针,都能够正确的访问到 __block 修饰的变量。
总结
1、当没有应用外部变量时是全局Block。
2、当 __weak 修饰Block 时是栈区Block
3、其他情况属于堆区Block
4、Block的本质是一个封装了实现和应用外部变量的结构体
5、__block 修饰的变量会被封装成一个结构体,并且Block结构体中有一个指针始终指向它,所以可以修改变量。