iOS底层探索之Block(三)——Block的本质

50 篇文章 6 订阅
5 篇文章 2 订阅

Block的本质是什么吗?__Block底层又做了什么呢?

在之前的篇博客中,已经介绍了block的类型,也对产生block的循环引用的问题给出了几种解决方法,那么本篇博客将对block的底层原理进行分析。

Block循环引用问题
iOS底层探索之Block(一)——初识Block(你知道几种Block呢?)

iOS底层探索之Block(二)——如何解决Block循环引用问题?

1. 通过block底层结构看本质

在分析block的原理之前,我们得看看block的底层结构是什么样的,还是老规矩 clang一下如下代码:

int main(int argc, const char * argv[]) {
	@autoreleasepool {

		int age = 8;
		void(^block)(void)= ^{
			printf("age:%d",age);
		};
		block();

	}
	return 0;
}

使用clang -rewrite-objc main.m -o main.cpp命令之后,可以很清楚的看到底层的代码结构,如下:
block底层结构

从图中看出是有类型的强转,那么我们去掉类型的强转,还原成最简单的结构去看看,如下:

去掉类型强转
去掉类型的强转,可以看出来block是一个__main_block_impl_0函数的调用,里面有三个参数,分是__main_block_func_0&__main_block_desc_0_DATAage

block本质
cpp文件里面,可以很明显的看出block是一个定义为__main_block_impl_0的结构体,该结构体继承自__block_impl

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
  • 在结构体中提供了一个构造函数__main_block_impl_0,这个构造函数对block结构体中相关属性进行设置。
  • 构造函数__main_block_impl_0的第一个参数为__main_block_func_0方法实现地址,在声明定义block时,将block的任务函数封装到FuncPtr属性中。
  • 我们调用自己的block的时候,实际上调用的是block->FuncPtr,并将block结构体作为参数传入到方法实现中。
void(*block)(void)= __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age));
 block->FuncPtr(block);

2. block捕获外部变量

我都知道block是具有捕获外部变量的能力的,从我们的结构体中可以看到,我们在外部的 int age,在 block的结构体中也有一个一模一样的age,这是为什么呢?

  • 当捕获外部变量时block结构体中会多一个成员变量age,并且构造函数也会多一个参数age,在构造函数__main_block_impl_0中外部传入的_age,赋值给成员 age,语法是age(_age),这是 c++的语法。
    __main_block_impl_0

  • 如果没有__block修饰,则通过值拷贝的方式,对其成员变量age进行赋值,在执行block任务时,从结构体中获取对应的成员变量__cself->age,进行处理。

捕获变量,在编译阶段就自动生成了相应的属性变量,来存储外界捕获的值,属于值拷贝。
捕获变量

这里是不能对age进行赋值变更的,因为是值拷贝,在内部和外部会有相同的变量值,编译不过会报错!
编译不过

3. __block 修改外部变量

block内部需要对外界的变量进行赋值,必须使用__block修饰:
__block 修改外部变量
默认情况下,在block中访问的外部变量是写操作不对原变量生效的,但是你可以加上 __block是可以让其写操作生效的,这又是为什么呢?我先去看看加上__block之后的底层结构是怎么样的,如下所示:
__block 之后的底层结构

  • 当外部变量使用__block修饰时,会封装成一个结构__Block_byref_age_0

  • block结构体中,多出一个属性age,属性age的类型为__Block_byref_age_0

  • age的地址会赋值到__Block_byref_age_0结构体的__forwarding属性中去,就是指向同一片内存空间,以达到修改外部变量值的作用。

  • 函数式保存,如果 block 不调用,函数是不会执行的,也就不会改变外部的变量的值了。

block通过函数保存
block 定义出来,这里通过 fp的函数来保存对外部变量的操作,我们手动调用block其实就是调用这个函数,也就是图中的FuncPtr,我们不就行调用blockblock是不会去调用这个功能逻辑代码的。FuncPtr的调用,传入的参数是block自身,在文章前面也介绍了 block的结构体是继承自__block_impl,如下:
FuncPtr调用
__block修饰符修饰的变量在编译时是一个栈 block,捕获到了之后,要对其进行变更操作,运行时就会拷贝到堆区
在这里插入图片描述
从打印结果可以得出是属于地址拷贝,在使用该变量时,实际使用的是指针的方式访问,因此在block中改变该变量的值是可以的,因为修改是同一片地址上的值。

还记得之前博客中的举例为什么第二个打印的值为3吗?
举例
在外部被创建好以后引用计数为1,因为objc没有使用__block进行修饰这时是通过值拷贝的方式进行处理+1block捕获了外部变量,最后在运行时会从栈区拷贝到堆区,这样objc的引用计数会再次加1,所以最后objc的引用计数为3

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卡卡西Sensei

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值