美文网首页iOS
iOS底层探索之Block(三)——Block的本质

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

作者: 俊而不逊 | 来源:发表于2021-08-31 10:41 被阅读0次

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

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

    Block循环引用问题.png

    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

    更多内容持续更新

    🌹 喜欢就点个赞吧👍🌹

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

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

    相关文章

      网友评论

        本文标题:iOS底层探索之Block(三)——Block的本质

        本文链接:https://www.haomeiwen.com/subject/wtfziltx.html