type
status
date
slug
summary
tags
category
icon
password
在前几篇,针对Block截获变量的类型已经作了全部说明——包括基本类型和对象类型。
下面,我们介绍Block中常用的一个修饰符
__block
。一、_block
的使用
在开发中,Block内部能直接修改全局变量或者static变量,而对auto变量,就不能。
__block
就用于在Block
内部修改auto
变量的值。上面展示了,
__block
是如何修改auto
变量的。二、__block
的底层结构
2.1 Block结构体
先观察
Block
对象的结构,以及转换后的__block
变量结构体:从上面的结构体中可以看出,
age
和person
最后都转换成了一个新的结构体。而且这个新的结构体的第一个成员变量是isa
指针,即这个是一个对象。所以得出第一个结论:结论1 :
__block
变量转换为一个对象__Block_byref_**;整个转换过程和结构如下:
__Block_byref_***
对象其中各个元素如上图:isa
指针
__forwarding
指针
- val是原变量
_size
及_flags
不是很重要的元素,忽略不谈。
2.2 Block代码中调用
以上代码,我们之前见过很多次,但是不同于之前的讨论过的情形:
- 全局变量,Block不会捕获,使用时直接访问;
auto
基本类型变量,Block捕获其变量,存储于Block内部,是值传递;
static
基本类型局部变量,Block捕获其变量指针,存储于Block内部,是指针传递;
auto
对象类型,Block捕获其变量及其所有权修饰符,即强引用、弱引用等修饰符;
然而在此处,Block针对
__block
变量,则是另一种处理方式,将其封装为为对象后,使用时访问该变量又略有不同。(age->__forwarding->age) = 30;(person->__forwarding->person = ....
我们整理了如下图:
2.3 __block
变量对象结构
2.3.1isa
isa
指针作为对象的标记之一,不用多说。2.3.2__forwarding
这是一个很奇怪的指针,从图中可以看出,它指向的是自己,如下图所示:
为什么会指向自己呢,原因是当栈中的Block复制到堆中的时候,在栈中仍然能正确访问堆中的变量。
下面我们就针对此,做一个小实验:
上面打印的结果:
可以看出,在栈中和堆中
age
的值只有一个,地址也是相同的。即均指向堆中的age
值,也要特别注意,这个age
变量,是__block修饰后,转换的结构体中的age变量,而不同于未加__block修饰,直接存在于栈中的age变量值。2.3.3 val
我们在上面
Block
代码调用封装__block
变量的值时,是通过__forwarding
指针调用的,如下:(age->__forwarding->age) = 30;(person->__forwarding->person = ....
val
就指封装成对应的__Block_byref_***
后原来变量的值。比如age
,就是__Block_byref_age_0->age
。三、__block
变量内存管理
上面在描述
__block
变量封装成对象后,一直没有讲述Block
对象对应的Desc
结果,根据之前的文章,我们了解到Desc
就是描述如何管理内存的结构体。我们先回顾一下之前的一些捕获变量或对象是如何管理内存的。注:下面“干预”是指不用程序员手动管理,其实本质还是要系统管理内存的分配与释放。
auto
局部基本类型变量,因为是值传递,内存是跟随Block,不用干预;
static
局部基本类型变量,指针传递,由于分配在静态区,故不用干预;
- 全局变量,存储在数据区,不用多说,不用干预;
- 局部对象变量,如果在栈上,不用干预。但
Block
在拷贝到堆的时候,对其retain
,在Block
对象销毁时,对其release
;
在这里,
__block
变量呢?很简单,看
Desc
,但也要注意一个点:注意点就是:
__block
变量在转换后封装成了一个新对象,内存管理会多出一层。3.1 基本类型的Desc
上述
age
是基本类型,其转换后的结构体为:而
Block
中的Desc
如下:针对基本类型,以
age
为例:__Block_byref_age_0
对象同样是在Block对象
从栈上拷贝到堆上,进行retain
;
- 当
Block对象
销毁时,对__Block_byref_age_0
进行release
;
__Block_byref_age_0
内age
,由于是基本类型,是不用进行内存手动干预的。
3.2 对象类型的Desc
下面看
__block
对象类型的转换:当然,针对
Desc
,在上文 3.1 基本类型的Desc 中已经贴出来了。但我们还观察到,因为捕获的本身是一个对象类型,所以该对象类型还需要进行内存是的干预。这里有两个熟悉的函数,即用于管理对象auto变量时,我们见过,用于管理对象auto的内存:
那么这两个函数对应的实现,我们也找出来:
3.2.1 初始化__block对象
下面针对转换来转换去的细节做了删减,方便阅读:
我们注意观察,在
__Block_byref_id_object_copy_131
和__Block_byref_id_object_dispose_131
函数中,都会偏移40字节,我们再看__block BFPerson
对象转换后的__Block_byref_person_1
结构体发现,其40字节偏移处就是原本的BFPerson *person
对象。3.2.2 对象类型的内存管理
以
BFPerson *person
,在__block
修饰后,转换为:__Block_byref_person_1
对象:__Block_byref_person_1
对象同样是在Block对象
从栈上拷贝到堆上,进行retain
;- 当
__Block_byref_person_1
进行retain
同时,会将person
对象进行retain
- 当
Block对象
销毁时,对__Block_byref_person_1
进行release
- 当
__Block_byref_person_1
对象release
时,会将person
对象release
3.2.3 与auto对象变量的区别
四、从栈到堆
Block从栈复制到堆时,__block变量产生的影响如下:
__block变量的配置存储域 | Block从栈复制到堆的影响 |
栈 | 从栈复制到堆,并被Block持有 |
堆 | 被Block持有 |
4.1 Block从栈拷贝到堆
当有多个Block对象,持有同一个__block变量。
- 当其中任何Block对象复制到堆上,__block变量就会复制到堆上。
- 后续,其他Block对象复制到堆上,__block对象引用计数会增加。
- Block复制到堆上的对象,持有__block对象。
4.2 Block销毁
4.3 总结
五、更多的细节
5.1 __block
捕获变量存放在哪?
上面代码输出:
可以看到,不管是
age
还是person
,均在堆空间。其实,本质上,将
Block
从栈拷贝到堆,也会将__block
对象一并拷贝到堆,如下图:5.2. 对象与__block变量的区别
测试下面代码,测试代码-__block与对象
转换后:
从上面可以得出:
参考
示例源码
1.__block