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_DATA
、age
。
从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
,我们不就行调用block
,block
是不会去调用这个功能逻辑代码的。FuncPtr
的调用,传入的参数是block
自身,在文章前面也介绍了 block
的结构体是继承自__block_impl
,如下:
__block
修饰符修饰的变量在编译时
是一个栈 block
,捕获到了之后,要对其进行变更操作,运行时
就会拷贝到堆区
。
从打印结果可以得出是属于地址拷贝,在使用该变量时,实际使用的是指针的方式访问,因此在block
中改变该变量的值是可以的,因为修改是同一片地址上的值。
还记得之前博客中的举例为什么第二个打印的值为3
吗?
在外部被创建好以后引用计数为1
,因为objc
没有使用__block
进行修饰这时是通过值拷贝
的方式进行处理+1
,block
捕获了外部变量,最后在运行时会从栈区拷贝到堆区
,这样objc
的引用计数会再次加1,所以最后objc
的引用计数为3
。
更多内容持续更新
🌹 喜欢就点个赞吧👍🌹
🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹
🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹