学习MJ的视频课程,整理总结知识点–__block的底层实现
本文根据前面所学的block的特性,结合Demo来讲解关于block的底层的一些东西。包括block的底层源码,
__block
关于block还要一个比较常见的场景就是block修饰,这一部分参见[block底层-Demo](https://cdn.ticsmatic.com/source/2020-05-21/diceng/day09/Interview04-__block.zip)
1 | int age = 10; |
我们知道如果不加__block,编译器是会报错。根据前面所学的block变量捕获,我们知道当block访问的外部变量是auto类型时,如果这个变量是基本类型,block会先把这个变量捕获到内部,所以block内部的age变量和外边的age此时已经不属于同一块内存空间了,自然就不能修改。
那怎么才能修改age的值,有以下两种方法
1.用static或全局变量修饰age变量
2.用__block修饰age变量
第1种方法我们我们知道此时block内部要么捕获age的指针地址,要么不捕获age,直接访问age,都是可以对age进行修改。
第2种加上__block后为什么也可以呢?我们通过源码进行分析这一过程,首先还是用clang导出block源码,如下
1 | struct __main_block_impl_0 { |
而此时__Block_byref_age_0
的结构如下
1 | struct __Block_byref_age_0 { |
而此时block内部的代码如下
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
定义__block int age = 10;
转化为源码如下
1 | __Block_byref_age_0 age = { |
我们发现,用block修饰后,age被包装成`Block_byref_age_0 *age;,变为结构体,而这个结构体内部存放的有age的值。block内部的age变为
Block_byref_age_0 *age; // by ref。 block函数实现部分
age->forwarding指向自己的结构体地址,可以通过
(age->forwarding->age) = 20;`修改内部age的值。这也解释为什么block可以修改值的本质原因。
概述一下block修饰的作用:
__block修饰的变量会被包装成一个对象(`Block_byref_xx_0),这个对象的结构体内部有用于存储真实的变量的成员。block内部有
__Block_byref_xx_0成员变量用来访问和修改变量
xx`。
__block修饰符小结:
- __block可以用于解决block内部无法修改auto变量值的问题
- 编译器会将block变量包装成一个对象(`Block_byref_xx_0`)
- __block不能修饰全局变量、静态变量(static)
再用一张图片做一下总结
__block修饰的变量的地址
我们通过下面这段代码研究输出的age
的地址
1 | __block int age = 10; |
这段代码的底层源码上面我们已经分析过了,block内部有一个__Block_byref_age_0 *age; // by ref
的成员变量,而__Block_byref_age_0
结构体内部有一个int age;
的成员变量。那么上面的代码输出的age地址是__Block_byref_age_0
的地址,还是__Block_byref_age_0
内部的int age
的地址?
其实我们分析也可以得知,输出的应该是__Block_byref_age_0
内部的int age
的地址。
一方面OC的语言设计者是不希望暴露block的内部细节给开发者,因为开发者平时没必要关注内部的细节。还有KVO动态生成子类,但是返回的class还是原来的类也是这样,为了屏蔽这个动态的子类。另一方面,我们在block内部给age赋值时,block源码内部也是通过结构体拿到int age
进行赋值操作,所以读取的话,理论上应该也是返回这个int age
。
我们可以通过实现block的源码来验证一个观点,参见__block转结构体-Demo
__block
内存管理
如果认真看__block
编译后的源码会发现,__block修饰的变量的底层源码和block访问对象类型的auto变量的底层源码有很多的相似之处。
参见对比源码中__block底层-Demo中
main.cpp
文件
比如__block的变量会被包装成对象,拥有这个对象,对这个对象进行内存管理。block源码内部都会有copy和dispose函数等。block内部都会通过_Block_object_assign
函数引用者block要访问的外部变量。block销毁时,也都会调用_Block_object_dispose
函数进行解除引用。
但是也是有一些不同点,对__block的内存管理可以总结如下:
当block在栈上时,并不会对__block变量产生强引用
当block被copy到堆时
- 会调用block内部的copy函数
- copy函数内部会调用_Block_object_assign函数
- _Block_object_assign函数会对__block变量形成强引用(retain)
- 当block拷贝到堆上时,内部访问的变量如果在栈上也会拷贝到,如果在堆上,引用计数会+1。这样来达到block来管理内部访问的变量
)
当block从堆中移除时
- 会调用block内部的dispose函数
- dispose函数内部会调用_Block_object_dispose函数
- _Block_object_dispose函数会自动释放引用的block变量(release)

__forwarding
我们获取在前面已经发现,__block
变量内部默认有一个__forwarding
指针,而且在block实现内部,比如上面我们修改age
的值时,也是通过__forwarding
来访问age的((age->__forwarding->age) = 20;
)。
为什么不直接通过结构体访问里面的age
,而是间接通过__forwarding
指针来访问age
?
对象类型的auto变量、__block变量小结
当block在栈上时,对它们都不会产生强引用
当block拷贝到堆上时,都会通过copy函数来处理它们
- __block变量(假设变量名叫做a)
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
- 对象类型的auto变量(假设变量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
当block从堆上移除时,都会通过dispose函数来释放它们
__block变量(假设变量名叫做a)_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
对象类型的auto变量(假设变量名叫做p)_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
__block修饰对象类型
1 | __block MJPerson *weakPerson = person; |
当__block变量在栈上时,不会对指向的对象产生强引用
当__block变量被copy到堆时
- 会调用__block变量内部的copy函数
- copy函数内部会调用_Block_object_assign函数
- _Block_object_assign函数会根据所指向对象的修饰符(strong、weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)
如果block变量从堆上移除
会调用block变量内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放指向的对象(release)
循环引用
因为block默认会对访问的对象进行强引用,如果此时对象已经对这个block强引用了,很容易就造成循环引用,从而导致内存泄露。如下
1 | MJPerson *person = [[MJPerson alloc] init]; |
这段测试代码就会有循环引用的问题,因为block内部访问对象类型的person
的变量,block此时默认会强引用这个person
对象,而这个block又是person
的成员变量,所以person会强引用着block。这就出现了循环引用。
ARC下循环引用的解决
__weak
通常的解决方法就是不让block强引用person
对象,用__weak修饰person对象,如下
1 | MJPerson *person = [[MJPerson alloc] init]; |
这样就打破了循环引用了,对象就可以正常的释放了。
__unsafe_unretained
解决循环引用还有另一种方法,就是使用__unsafe_unretained
,如下
1 | __unsafe_unretained typeof(person) weakPerson = person; |
使用__unsafe_unretained
,block就不会对person
产生强引用。但是它不安全,体现在当block内的weakPerson
销毁时,block内指针存储的地址值不变,此时访问weakPerson
就可能出现坏内存访问的问题。而用__weak时,指向的对象销毁时,会自动让指针置为nil。
1 | // __weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil |
___block
解决循环引用还有一种方法就是使用___block
修饰,如下:
1 | __block typeof(person) weakPerson = person; |
分析:
此时内部会有3个对象,分别是block对象、block对象、block对象内部的person对象。
其中block对象会强引用着block对象,block对象内部会强引用着person对象,但是我们上面的代码,在block的实现里面,当block内代码走完时,手动把内部的person指针置为nil,此时block就不会对person对象强引用,就可以打破循环引用了。

MRC下循环引用的解决
MRC不支持__weak,
- 可以使用__unsafe_unretained
1 | __unsafe_unretained typeof(person) weakPerson = person; |
- 可以使用_block,因为block修饰对象类型的变量,在MRC下是不会自动持有对象的
1
2
3
4__block typeof(person) weakPerson = person;
person.block = ^{
NSLog(@"age is %d", weakPerson.age);
};补充
在解决循环引用时,有一种常见的写法是在block内部使用__strong,我们看这段代码
1 | __weak typeof(self) weakSelf = self; |
解决循环引用时,使用weak类型的对象,但是当执行block内部的逻辑代码时,如果self被释放,此时block访问释放的self就会挂,在block里面使用的strong修饰的weakSelf是为了在函数生命周期中防止self提前释放。strongSelf是一个自动变量当block执行完毕就会释放自动变量,strongSelf不会对self进行一直进行强引用。
总结
block本质上也是一个OC对象,它内部也有个isa指针
block是封装了函数调用以及函数调用环境的OC对象
block的底层结构如图所示