本文主要简述Block、__block的本质是什么东西,文章涉及循环引用等开发常见问题,需要重点关注。
先写一个最简单的block
1 | void(^block)(void) = ^{ |
使用命令行将main.m
文件编译成C++文件xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
编译完成后,上述代码会变成以下结构
1 | void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)); |
可以看出是生成了一个__main_block_impl_0
结构体类型的对象,参数1为__main_block_func_0
,参数2为&__main_block_desc_0_DATA
block的底层结构
接下来查看一下__main_block_impl_0
是什么东西
1 | /// block的结构体,引用了两个结构体和实现了一个构造方法 |
所以拼接一下可以看出
1 | struct __main_block_impl_0 { |
- 结论1:block本质上是一个OC对象,它内部也有个isa指针
接下来看看构造方法,第一个参数是void *fp
意为function pointer
函数指针,所以我们回去看看这里传了什么值进去
1 | void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)); |
是__main_block_func_0
,那我们再看看__main_block_func_0
是什么
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
可以看出我们在block内写的代码被封装成了一个函数
- 结论2:block是封装了函数调用以及函数调用环境的OC对象
最后,看一下block的调用,被编译成了下述结构(这里我们去掉强制转换)
1 | (block)->FuncPtr(block); |
可以看出,是取出了变量中的FuncPtr
成员变量,得到函数指针后把自己传进去,这样子就完成了block的调用
block捕获外部变量
从上面的分析我们可以得知,局部变量的定义和使用,是在两个函数中进行的,所以为了能够给让变量跨函数访问,block需要捕获该变量
局部auto类型的变量捕获
auto : 自动变量,离开作用域自动销毁
平常我们定义的局部变量,默认都是auto
修饰的
先写一个简单的demo
1 | int age = 10; // 等价于 auto int age = 10; |
编译后变成如下结构(去掉强制转换)
1 | int age = 10; |
__main_block_impl_0
也发生了变化,可以看到多了一个age
成员变量,所以构造函数也多了一个参数
1 | struct __main_block_impl_0 { |
age(_age)
这个语法意为将构造方法传进来的_age
赋值到自己的的成员变量age
,这个过程我们称作变量的捕获
block内编译后的函数__main_block_func_0
也发生了变化
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
可以看出函数内取出了结构体内的age
成员变量,然后交给NSLog
使用
由此可知,block在定义的时候就会把外部传进来的参数给存储一遍,然后调用的时候在从block对象中取出来,所以这就意味着被block捕获的变量,在被捕获后如果修改了值,是不会应用到block中的,举例如下
1 | int age = 10; |
局部static类型的变量捕获
把上面的age
变量加一个static
修饰符试试看
1 | static int age = 10; |
结果会输出age = 20
,原因是静态变量会将static
修饰的对象转为block的指针类型的成员属性,如下
1 | struct __main_block_impl_0 { |
因为是指针传递,所以当指针指向的内容变化时,打印出来的值也就会变化了
全局变量不捕获
全局static
变量不捕获,因为全局static
变量大家都能访问,所以函数内可以直接读取值
block内的self会被捕获吗
会,我们来举个例子
1 | - (void)test { |
因为test
方法会添加两个隐含的参数,编译后如下
1 | static void _I_Person_Test(Person *self, SEL _cmd) { |
所以这就是为什么我们能在方法内访问self
和_cmd
的原因,因为方法会传进来self
参数,又因为参数是局部变量,又因为局部变量会被捕获,所以self
参数会被捕获,捕获后大概长这样子
1 | struct __main_block_impl_0 { |
Block的类型
Block有三种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
存储区域 | 类名 | 特点 | 调用copy后的效果 |
---|---|---|---|
数据区域 .data区 | __NSGlobalBlock__ |
没有访问auto变量 | 无效果 |
栈区 | __NSStackBlock__ |
访问了auto变量 | 从栈复制到堆 |
堆区 | __NSMallocBlock__ |
__NSStackBlock__ 调用了copy |
引用计数增加 |
__NSGlobalBlock__
1 | void(^block)(void) = ^{ |
__NSStackBlock__
1 | int age = 20; |
因为在ARC环境下,栈区block会自动copy,所以要测试这个类型的时候,需要使用MRC环境
栈区数据的特点是会自动销毁,离开了作用域,数据都会销毁
栈区block存在的问题是,捕获的变量会存放在栈区,所以一旦离开了作用域,捕获的内容就销毁了,将来再去访问这个block内捕获的变量,访问到的可能就是一个未知的内容
以下情况编译器会自动将栈上的block复制到堆上:
- block作为函数返回值时,比如
1
2
3
4
5
6
7typedef void(^Block)(void);
Block myBlock() {
int age = 10
return ^{
NSLog(@"---------%d",age);
};
} - 将block赋值给__strong指针时
- __strong是是id类型和对象类型默认的所有权修饰符,所以平时在ARC环境下写的引用外部auto局部变量的block都会自动copy到堆中,原因是block默认被__strong修饰了
- block作为Cocoa API中方法名含有usingBlock的方法参数时
- 比如NSArray的
enumerateObjectsUsingBlock:
方法,block传进去之后就会被copy一下
- 比如NSArray的
- block作为GCD API的方法参数时
- 比如GCD的
dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
方法,block传进去之后就会被copy一下
- 比如GCD的
__NSMallocBlock__
将__NSStackBlock__
进行一次copy,即可得到__NSMallocBlock__
1 | int age = 20; |
捕获对象类型的auto变量
当block内部访问了对象类型的auto变量时,如下
1 | Person *person = [[Person alloc] init]; |
我们在ARC环境下编译,这时候block会在堆上(因为被自动copy了),执行命令行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
,会发现block结构体的person属性多了一个__strong
的修饰符,证明他被block强持有了。
1 | struct __main_block_impl_0 { |
但是我们如果编译的时候去掉-fobjc-arc
,默认就是MRC环境了,这时候block会在栈上,查看编译后的c++文件发现不会有__strong
修饰符,如果我们在block执行前加一行[person release]
,那么这时候person
就会直接释放,证明block没有持有person变量
- 结论1:如果block是在栈上,将不会对auto变量产生强引用
如果我们给person添加__weak
修饰符
1 | __weak Person *person = [[Person alloc] init]; |
则block将会对person对象进行弱引用,编译后如下:
1 | struct __main_block_impl_0 { |
所以使用__weak
修饰符可以避免block对外部变量的强引用操作
我们来看看block的结构体内的__main_block_desc_0
是个什么东西
1 | static struct __main_block_desc_0 { |
可以发现,多了两个函数指针,copy
和dispose
,分别指向__main_block_copy_0
和__main_block_dispose_0
这两个函数的实现如下
1 | static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { |
_Block_object_assign
函数内部会对person进行引用计数器的操作,如果__main_block_impl_0
结构体内person指针是__strong
类型,则为强引用,引用计数+1,如果__main_block_impl_0
结构体内person指针是__weak
类型,则为弱引用,引用计数不变。
_Block_object_dispose
会对person对象做释放操作,类似于release,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数
- 结论2:如果block被拷贝到堆上
- 会调用block内部的copy函数
- copy函数内部会调用_Block_object_assign函数
- _Block_object_assign函数会根据auto变量的修饰符(
__strong
,__weak
,__unsafe_unretained
)做出相应的操作,类似于retain(形成强引用、弱引用)
__unsafe_unretained修饰的变量不会增加引用计数,当销毁时,该指针不会置空,会造成不安全的情况。
- 结论3:如果block从堆上移除
- 会调用block内部的dispose函数
- dispose函数内部会调用_Block_object_assign函数
- _Block_object_dispose函数会自动释放引用的auto变量,类似于release
函数 | 调用时机 |
---|---|
copy函数 | 栈上的Block复制到堆时 |
dispose函数 | 堆上的Block被废弃时 |
__block
一般情况下我们是无法改变block捕获的外部的值的
1 | Person *person = [[Person alloc] init]; |
从上面的内容我们也可以知道原因。就是因为外部的person所在的内存空间和block内(单独开辟了一个函数)的内存空间不在同一个位置,所以block内是访问不到的外部的person的。
但是我们如果添加了__block
关键字的话,就可以访问了,所以我们编译一下看看添加__block
之后的情况
1 | __block Person *person = [[Person alloc] init]; |
编译后长这样
1 | struct __main_block_impl_0 { |
可以看到person对象被封装成了一个__Block_byref_person_0 *
类型的属性
继续看看__Block_byref_person_0
是什么
1 | struct __Block_byref_person_0 { |
__Block_byref_person_0
有isa指针,是一个OC对象,里面有一个强引用的person对象(指向的内容等同于外面的person指针指向的内容),和我们熟悉的__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
方法用于处理内存管理问题,还有指向自身的forwarding
指针(这个指针指向对象自身),flag
和size
,分别表示标记位和这个结构体的占用内存空间大小。
person被封装成了结构体对象之后,原先的block函数就变成了
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
可以看出,但我们要改变person指针的值的时候,首先是取出person对象,即__Block_byref_person_0 *person
,然后再通过forwarding
指针拿到自己,再拿到最里面的person,最后就可以修改了。
通过内存打印,我们可以得知
__Block_byref_person_0
内的person对象和外部的person对象的地址是一致的,所以我们在block内修改外部的person就是相当于修改__Block_byref_person_0
内的person对象为什么要绕一个圈,不直接
person->person
而是要person->__forwarding->person
?原因是防止block从栈复制到堆之后,栈上面的block访问person访问到的是栈上的person而不是堆上的person,所以栈上的forwarding指针要指向堆的block,这样子就能一直访问到堆上的person了封装的
__Block_byref_person_0
结构体内的__Block_byref_id_object_copy
函数会管理他自己的person对象的内存,实现代码如下:
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
,这里的40是person对象的偏移值,可以看到结构体内person前面的4个指针加2个整型刚好40个字节。__Block_byref_person_0
在__main_block_impl_0
内必定是强引用,跟我们上面所说的不一样,就算在__block
之前再添加__weak
修饰,__Block_byref_person_0
在__main_block_impl_0
内依旧是强引用,加上__weak
修饰受影响的只有__Block_byref_person_0
内的person指针的引用方式
注意:MRC环境下,__block 不会对变量造成强引用,即以下情况person会提前释放
1 | __block Person *person = [Person new]; |
注意:__block只能用于修饰auto变量,不能修饰全局变量和静态(static)变量
循环引用
循环引用的发生原因与解决方式
以下代码会产生循环引用
1
2
3@interface Person : NSObject
@property (nonatomic, copy) void(^block)(void);
@end1
2
3
4
5Person *person = [Person alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"age is %d",person.age);
};原因是person.block捕获了person,person又持有着block,也就是block内部对person存在一个强引用,person对block也存在一个强引用,所以均无法释放。
在定义person指针的时候添加
> __weak: 不会产生强引用,指向的对象销毁时,会自动让指针至nil,不支持MRC > __unsafe_unretain: 不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变,变成野指针,支持MRC__weak
修饰符或者__unsafe_unretain
修饰符,就可以让block在捕获person的时候弱引用person,这样子就不会造成循环引用了.在定义person指针的时候添加
__block
修饰符也可以解决循环引用问题,但前提是需要调用block并手动将person指针置为nil,如下1
2
3
4
5
6
7__block Person *person = [Person alloc] init];
person.age = 10;
person.block = ^{
NSLog(@"age is %d",person.age);
person = nil;
};
person.block();为什么这样子可以解决循环引用呢?首先先分析一下内存结构
- peron持有block
- block持有__block对象
- __block持有着person
- peron又持有着block
- …
所以这里是三个对象相互持有形成一个三角形关系
当block执行,person = nil,解除了`__block`变量对person的引用的时候,循环引用就不再存在了- MRC环境下,由于**__block不会对变量造成强引用**,所以直接用__block修饰指针也可以达到以上效果
为什么block做属性不常用weak而是用copy
如果上述例子使用weak修饰block的话,那么block会在栈中,block里面的person也会在栈中,所以离开了作用域的话,里面的person就会销毁,从而无法使用。
如果你希望block做完事情就释放,比如发送一个通知,修改某个单例类的属性,没有引用外部局部变量,那么用weak就可以节约内存空间