美文网首页
iOS--block的本质和变量捕获机制

iOS--block的本质和变量捕获机制

作者: 人间四月天_Andy | 来源:发表于2021-02-24 16:31 被阅读0次

    学习笔记,如有错误,欢迎批评指正!!! 仅供学习交流...

    block的本质

    1. block是带有自动变量(局部变量)的匿名函数(不带名称的函数)
      1. 带有自动变量的值:block保持自动变量的值。
    2. block的本质是一个OC对象,它内部有一个isa指针。

    直接上代码,查看block的底层结构

    void (^block1)(void) = ^{
                NSLog(@"This is block!!!");
            };
    

    接下来,使用终端命令:

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o xxx.cpp
    

    然后,查看xxx.cpp文件,如下图所示(请忽略文件命名 🤣...)

    1.jpg
    接下来,我们把重点代码复制下来
    //block转化成c++的结构体
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      //构造函数(类似于OC的init方法)
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    ////封装了 block执行逻辑的函数
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_zt_xy2f_gsx0mlbrzmvts93gjqm0000gn_T_main_b2615a_mi_0);
            }
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            //定义block变量
            void (*block1)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
            //执行block内部的代码
            ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);
        }
        return 0;
    }
    

    对 main 函数里边的代码 进行简化:

    //定义block变量
    void (*block1)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
    
    //执行block内部的代码
    ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);
    

    继续简化:

    //定义block变量
    //调用 __main_block_impl_0 构造函数,返回结构体对象,最后取地址,复制给 block1
    //返回的结构体对象就是 struct __main_block_impl_0
    void (*block1)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
    
    //执行block内部的代码
    block1->FuncPtr(block1);
    

    block的底层机构大致就是这样的:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      //构造函数(类似于OC的init方法)
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    接下来,我们看一下这段代码:

    void (*block1)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
    

    可以看到,__main_block_impl_0 函数传递了两个参数 (void *)__main_block_func_0&__main_block_desc_0_DATA

    第一个参数 (void *)__main_block_func_0,这个函数里边封装的就是 block的执行逻辑代码

    最终 这个函数指针赋值给了 struct __block_impl里边的void *FuncPtr。也就是说 FuncPtr里边存储的就是将来要执行的 block函数的地址。

    2.jpg
    接着,我们看 block的执行代码
    ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);
    
    //简化之后
    block1->FuncPtr(block1);
    

    可以看到,执行block内部的代码,其实是找到 FuncPtr指针,然后去执行。

    block1之所以可以直接找到 FuncPtr,是因为 将 block1强制转换成了 __block_impl 这种类型,然后再找到 __block_impl里边的 FuncPtr

    __main_block_impl_0 这种类型的block 之所以 能够转换成为 __block_impl类型,是因为 __main_block_impl_0里边的第一个成员就是 __block_impl类型的,这两个的地址是一样的。

    从另一种方面来说,由于__main_block_impl_0类型的第一个成员的类型是 struct __block_impl,所以 __main_block_impl_0也可以直接看成是如下结构:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    struct __main_block_impl_0 {
      //struct __block_impl impl;
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
      struct __main_block_desc_0* Desc;
      //构造函数(类似于OC的init方法)
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    这样,就可以简单明了的理解 block为什么可以直接找到 FuncPtr了。

    其实,我们还可以通过以下代码查看 block

    void (^block1)(void) = ^{
                NSLog(@"This is block!!!");
            };
            
    block1();
                  
    NSLog(@"%@ %@ %@ %@",[block1 class]
                          ,[[block1 class] superclass]
                          ,[[[block1 class] superclass] superclass]
                          ,[[[[block1 class] superclass] superclass] superclass]);
    
    输出结果: 3.jpg

    可以看到,block最终继承自 NSObject,再次证明 block其实就是一个OC对象

    变量的捕获

    为了保证 block 内部能够正常访问 外部变量的值, block有个变量捕获机制

    C语言的函数中可能使用的变量

    • 自动变量(局部变量)-- 离开作用域就会自动销毁
    • 函数的参数
    • 静态局部变量
    • 静态全局变量
    • 全局变量

    其中,静态局部变量静态全局变量全局变量,虽然这些变量的作用域不同,但是在整个程序当中,一个变量总保持在一个内存区域。因此,即使在多个函数中使用这些变量,这些变量的值总是保持不变。

    那么,哪些变量可以被捕获到block内部呢?先看下边的结论

    局部变量会被捕获到block内部,全局变量不会被捕获到block内部。

    局部变量的捕获

    直接上代码: 4.jpg

    定义了 两个变量 a 和 b, 运行,查看输出结果

    02-block变量捕获[2020:161392] a = 10, b = 20
    

    接着看转换的c++代码。

    5.jpg
    可以看出,变量a 和 变量b 都被捕获到了 block 内部。并且,变量a 和 变量b 的捕获方式是一模一样的,都是直接把值传递进去(值传递)。
    //这两句代码是等效的  默认前边都是有一个 auto 关键字
    int a = 10; 
    auto int a = 10; 
    

    接下来,修改代码,如下图:

    6.jpg
    从运行输出结果可以看出 static修饰的局部变量 和 auto修饰的局部变量的结果是不一样的。

    重新生成一下xxx.cpp代码,查看一下:

    7.jpg
    可以看出static修饰的局部变量 传递的是 地址值,被block捕获到内部的是一个指针。(指针传递)

    关于局部变量的捕获:

    1. auto类型:会被捕获, 捕获方式:值传递
    2. static类型:会被捕获, 捕获方式:指针传递

    全局变量的捕获

    继续修改代码,如下如所示:

    8.jpg

    全局变量a 和 静态全局变量b 获取到的都是最新的值。

    重新生成cpp文件

    9.jpg
    可以看出,全局变量a 和 静态全局变量b 并没有被捕获到 block内部。

    总结:

    10.jpg

    结论 11.jpg

    相关文章

      网友评论

          本文标题:iOS--block的本质和变量捕获机制

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