iOS底层探索之Block——Block的本质

Posted 卡卡西Sensei

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS底层探索之Block——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是一个__main_block_impl_0函数的调用,里面有三个参数,分是__main_block_func_0&__main_block_desc_0_DATAage


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++的语法。

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

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

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

3. __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 定义出来,这里通过 fp的函数来保存对外部变量的操作,我们手动调用block其实就是调用这个函数,也就是图中的FuncPtr,我们不就行调用blockblock是不会去调用这个功能逻辑代码的。FuncPtr的调用,传入的参数是block自身,在文章前面也介绍了 block的结构体是继承自__block_impl,如下:

__block修饰符修饰的变量在编译时是一个栈 block,捕获到了之后,要对其进行变更操作,运行时就会拷贝到堆区

从打印结果可以得出是属于地址拷贝,在使用该变量时,实际使用的是指针的方式访问,因此在block中改变该变量的值是可以的,因为修改是同一片地址上的值。

还记得之前博客中的举例为什么第二个打印的 2吗?

在外部被创建好以后引用计数为1,因为objc没有使用__block进行修饰这时是通过值拷贝的方式进行处理+1block捕获了外部变量,最后在运行时会从栈区拷贝到堆区,这样objc的引用计数会再次加1,所以最后objc的引用计数为3

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

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

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

以上是关于iOS底层探索之Block——Block的本质的主要内容,如果未能解决你的问题,请参考以下文章

iOS底层探索之Block——Block源码分析(__block 底层都做了什么?)

iOS底层探索之Block——初识Block(你知道几种Block呢?)

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

iOS底层原理 - Block本质探究

iOS底层原理总结 - 探寻block的本质

iOS开发系列-Block本质篇