写在前面
在上一篇文章【iOS重学】Block底层原理(一)中我们主要讲了Block的基本使用、底层原理、对变量的捕获机制以及Block的几种类型,本文是第二篇,主要内容包含:
__block
修饰符的基本使用
__block
修饰的变量在Block内部的底层结构
__block
的内存管理
- 循环引用
__Block的基本使用
如果想在Block内部修改auto变量的值,我们一般是无法直接修改的,会报如下错误:

__block
修饰符就是用来解决Block内部无法修改auto变量值的问题。
__block
不能用来修饰全局变量、static变量。
__block修饰的变量底层结构探究
__block int age = 10; void(^Block)(void) = ^{ NSLog(@"age is %d",age); }; Block();
|
如上,使用__block
修饰的变量在Block内部之后的底层结构是什么样的呢?跟之前对比有什么不一样。
不使用__block
修饰符,Block底层结构如下:

使用__block
修饰符,Block底层结构如下:


我们发现底层结构确实发生了变化:被__block
修饰的变量会被包装成一个__Block_byref_age_0
的对象,这个对象的结构里面有个int age
,具体如下:
struct __Block_byref_age_0 { void *__isa; __Block_byref_age_0 *__forwarding; int __flags; int __size; int age; };
|
问题:
NSMutableArray *tempArr = [NSMutableArray array]; void(^Block)(void) = ^(){ [tempArr addObject:@"1"]; }; Block();
|
以上代码结果是否会报错?
不会,[tempArr addObject:@"1"]
只是在使用tempArr
指针并没有修改tempArr
。
__block int age = 10; NSLog(@"1---%p",&age); void(^Block)(void) = ^{ NSLog(@"age is %d",age); NSLog(@"2---%p",&age); }; Block(); NSLog(@"3---%p",&age);
|
我们打印age
的地址看一下:
2022-12-08 14:34:52.006645+0800 BlockDemo[8781:6613854] 1---0x7ff7bfeff2d8 2022-12-08 14:34:52.007272+0800 BlockDemo[8781:6613854] age is 10 2022-12-08 14:34:52.007357+0800 BlockDemo[8781:6613854] 2---0x100b478a8 2022-12-08 14:34:52.007395+0800 BlockDemo[8781:6613854] 3---0x100b478a8
|
从打印结果我们看到1和2、3的age的地址值不一样,我们可以根据上面的底层结构探索来解释一下为什么?
struct __Block_byref_age_0 { void *__isa; struct __Block_byref_age_0 *__forwarding; int __flags; int __size; int age; };
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; };
struct __main_block_desc_0 { size_t reserved; size_t Block_size; };
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; struct __Block_byref_age_0 *age; };
__block int age = 10; NSLog(@"1---%p",&age); void(^Block)(void) = ^{ NSLog(@"age is %d",age); NSLog(@"2---%p",&age); };
struct __main_block_impl_0 *implBlock = (__bridge struct __main_block_impl_0 *)Block;
|
我们把Block转为__main_block_impl_0
的结构体来分析一下:

Block内部的__Block_byref_age_0
结构体地址值是:0x100e1c7e0
,而我们打印age的地址值是:0x100e1c7f8
,两个不一样,说明打印的age的地址值不是__Block_byref_age_0
结构体age的值,接着往下分析:
struct __Block_byref_age_0 { void *__isa; struct __Block_byref_age_0 *__forwarding; int __flags; int __size; int age; };
|
通过上面的分析我们看到:我们打印的age的地址值其实是__Block_byref_age_0
结构体中age的地址值。
__block的内存管理
在上面我们分析的是基本数据类型用__block
来修饰,我们接下来看一下更复杂的情况:__block
用来修饰对象类型。
__block NSObject *object = [[NSObject alloc] init]; void(^Block)(void) = ^{ NSLog(@"object is %@",object); }; Block();
|
底层结构如下:

这里有两点值得我们注意一下:
1的位置多了两个函数copy
和dispose
,这点我们在上一篇文章讲到过因为Block捕获的变量是对象类型,所以会有这两个函数,这里我们就不赘述了,除此之外我们发现__Block_byref_object_0
这个结构体里面也多了两个函数:__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
,里面具体实现如下:
static void __Block_byref_id_object_copy_131(void *dst, void *src) { _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131); } static void __Block_byref_id_object_dispose_131(void *src) { _Block_object_dispose(*(void * *) ((char*)src + 40), 131); }
|
内部也是调用的_Block_object_assign
和_Block_object_dispose
,和我们之前讲的是一样的。
并且我们看到__Block_byref_object_0
结构体里面会对object
这个对象有一个强引用。
下面我们来总结一下:
1、当Block在栈上时,并不会对__block
修饰的变量产生强引用。
2、当Block被copy到堆上时,会调用Block内部的copy
函数,copy
函数内部会调用__Block_object_assign
函数,__Block_object_assign
函数会对__block
修饰的变量形成强引用。


3、当Block从堆中移除时,会调用Block内部的dispose
函数,dispose
函数内部会调用__Block_object_dispose
函数,__Block_object_dispose
会对__block
修饰的变量进行一次release操作。


Block的循环引用
@interface Person : NSObject
@property (nonatomic, assign) int age; @property (nonatomic, copy) void(^PersonBlock)(void);
@end
@implementation Person
- (void)dealloc { NSLog(@"%s",__func__); }
@end
int main(int argc, const char * argv[]) { @autoreleasepool { Person *person = [[Person alloc] init]; person.age = 10; person.PersonBlock = ^{ NSLog(@"person's age is %d",person.age); }; person.PersonBlock(); } NSLog(@"--------"); return 0; }
|
打印结果:
2022-12-08 16:07:45.878556+0800 BlockDemo[9541:6681377] person's age is 10 2022-12-08 16:07:45.879146+0800 BlockDemo[9541:6681377] --------
|
发现person
对象并没有被释放还存在内存里面,这就是我们常说的循环引用(内存泄漏)。
下图表示了上面对象之间的持有关系:

如何解决循环引用?
其实就是把2和3其中一个变成弱引用即可,那么到底2和3谁变成弱引用更合适呢,3是Person
对象有一个PersonBlock
这个属性,我们希望当这个Person
对象还在的时候随时能访问到PersonBlock
,所以3应该是个强引用,我们把2换成弱引用即可。
- 使用
__weak
,__unsafe_unretain
- 使用
_block
,但是必须调用block
Person *person = [[Person alloc] init]; __weak typeof(person) weakPerson = person; person.age = 10; person.PersonBlock = ^{ NSLog(@"person's age is %d",weakPerson.age); }; person.PersonBlock();
Person *person = [[Person alloc] init]; __unsafe_unretained typeof(person) weakPerson = person; person.age = 10; person.PersonBlock = ^{ NSLog(@"person's age is %d",weakPerson.age); }; person.PersonBlock();
__block Person *person = [[Person alloc] init]; person.age = 10; person.PersonBlock = ^{ NSLog(@"person's age is %d",person.age); person = nil; }; person.PersonBlock();
|
我们来分析一下__block
修饰的变量的内存问题:
__block Person *person = [[Person alloc] init]; person.age = 10; person.PersonBlock = ^{ NSLog(@"person's age is %d",person.age); }; person.PersonBlock();
|
用一张图来表示他们之间的引用关系:

如何解决循环引用?
Person *person = [[Person alloc] init]; __block __weak typeof(person) weakPerson = person; person.age = 10; person.PersonBlock = ^{ NSLog(@"person's age is %d",weakPerson.age); }; person.PersonBlock();
|
底层结构如下:

这样我们就可以解决Block带来的一些循环引用的问题啦。
写在最后
关于Block的底层原理在这里就全部结束了,如有错误请多多指教。