Block(五)__block变量
00 分钟
2022-11-15
2022-11-15
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变量结构体:
从上面的结构体中可以看出,ageperson最后都转换成了一个新的结构体。而且这个新的结构体的第一个成员变量是isa指针,即这个是一个对象。所以得出第一个结论:
结论1 :__block变量转换为一个对象__Block_byref_**;
整个转换过程和结构如下:
notion image
__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 = ....
我们整理了如下图:
notion image

2.3 __block变量对象结构

2.3.1isa

isa指针作为对象的标记之一,不用多说。

2.3.2__forwarding

这是一个很奇怪的指针,从图中可以看出,它指向的是自己,如下图所示:
notion image
为什么会指向自己呢,原因是当栈中的Block复制到堆中的时候,在栈中仍然能正确访问堆中的变量。
下面我们就针对此,做一个小实验:
上面打印的结果:
notion image
可以看出,在栈中和堆中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_0age,由于是基本类型,是不用进行内存手动干预的。

3.2 对象类型的Desc

下面看__block对象类型的转换:
当然,针对Desc,在[基本类型的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对象变量的区别

notion image

四、从栈到堆

Block从栈复制到堆时,__block变量产生的影响如下:
__block变量的配置存储域
Block从栈复制到堆的影响
从栈复制到堆,并被Block持有
被Block持有

4.1 Block从栈拷贝到堆

notion image
当有多个Block对象,持有同一个__block变量。
  • 当其中任何Block对象复制到堆上,__block变量就会复制到堆上。
  • 后续,其他Block对象复制到堆上,__block对象引用计数会增加。
  • Block复制到堆上的对象,持有__block对象。

4.2 Block销毁

notion image

4.3 总结

notion image

五、更多的细节

5.1 __block捕获变量存放在哪?

上面代码输出:
notion image
可以看到,不管是age还是person,均在堆空间。
其实,本质上,将Block从栈拷贝到堆,也会将__block对象一并拷贝到堆,如下图:
notion image

5.2. 对象与__block变量的区别

测试下面代码,测试代码-__block与对象
转换后:
从上面可以得出:
notion image

参考

示例源码