一篇就带你读懂关于block的变量捕获(capture)

简介: 一篇就带你读懂关于block的变量捕获(capture)

上面一节讲解了block的本质,举例了block最简单结构的一种情况。如果更复杂了呢。比如block函数执行调用外部参数,会有哪些情况呢?不同的情况,他们又有什么异同点呢?这里先把分析结果写在最前面,不懂的可以先跳过,看下面的正文每一个情形有底层结构分析。如果能看明白的,可以不看下面的正文,说明你已经很清楚 block 变量捕获机制了:


1、block变量捕获机制(capture)

a232add6b1cb4f39b5d39e7241f118cc.png


1、捕获(capture):是指在 block 内部会专门新增一个成员变量,来存储传进来的值。或者说将外面的值捕获到 block 结构体中存储。

2、{ ... } :函数作用域,内部声明的变量都是局部变量。

3、auto:又称自动变量。其作用是离开对应的作用域该变量就会被销毁(值传递)。

(c语言中,当定义出一个局部变量的时候,会默认添加,所以不再需要手动添加)


如下面两个定义的变量是等价的:


auto int age = 0;

int age = 0;

注意:auto只存在于局部变量中。

4、static:静态变量,又分静态局部变量,静态全局变量


2、局部变量


1、只要是局部变量而且是 block 要访问的局部变量,block 都会将这些局部变量捕获到block结构体中。

2、捕获的区别就是 auto 进行的值传递,而 static 进行的是指针传递。


3、局部变量为什么会有自动变量和局部静态变量的差异呢?


1、auto 局部变量的内存随时可能被销毁,内存会消失。所以优先保证变量的值能拿到。

2、static 地址一直存在,可以在需要的时候取值,所以这里优先保证取得的数据最新。


4、为什么局部变量需要捕获,而全局变量不需要捕获?


1、局部变量:因为 block 应用存在跨函数访问,那么局部变量就有可能被销毁。为了使执行block的时候有值,所以才想到将这些变量捕获到block的结构体中,然后访问结构体中的变量。

2、全局变量:只要是全局变量,block内部不需要去捕获这些变量,可以直接进行访问。


5、关于 self 局部变量


1、在OC中我们可以看到,当函数转换成C++时,都默认带有两个参数,第一个参数是调用者本身self,第二个参数是 _cmd 这个方法的方法名称。就是说,OC函数也是默认有这两个参数的,只是默认隐藏不显示出来而已。因此我们可以知道 self 是局部变量,不是全局变量, self 指向的是方法调用者本身。

2、正因为 self 是局部变量,所以 self 会捕获到 block 里面去。


1、block执行的时候传递参数、


1.1、代码示例

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(int,int) = ^(int a,int b){
            NSLog(@"a:%d    b:%d",a,b);
        };
        block(10,10);
    }
    return 0;
}


1.2、代码转成C++

打开命令行,cd 到 main.m 的目录下,执行以下命令:


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

执行完命令后,在同目录下会生成 main.cpp 文件,然后将文件导入项目(注意:该文件不参与编译)。


代码转成c++,都是一样的命令,后面就不写了。


1.3、底层结构分析

// 1、程序入口
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
       //  2、定义 block 变量
        void(*block)(int,int) = &__main_block_impl_0(
                    __main_block_func_0,
                    &__main_block_desc_0_DATA);
        // 
        /**
        * 3、执行 block 变量
        * 调用封装到block的函数方法,通过上一节我们可以直接看出来,这里传递了两个参数
        */
        block->FuncPtr(block, 10, 10);
    }
    return 0;
}
// 4、block内部封装了的函数方法,传递了两个参数,然后在方法内部直接引用。
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_vc_pn677_yj1sz8hgf_q5bjvssc0000gn_T_main_ed1930_mi_0,a,b);
}


2、block内部引用局部变量


2.1、代码示例

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void(^block)(void) = ^{
            NSLog(@"age:%d",age);
        };
        age = 20;
        block();
    }
    return 0;
}


2.2、底层结构分析

// 程序入口
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        // tag-1、变量
        int age = 10;
        // tag-2、定义函数:执行结构体函数的时候,这里多了一个 age 值传递
        void(*block)(void) = &__main_block_impl_0(
          __main_block_func_0, 
          &__main_block_desc_0_DATA, 
          age// tag-2.1、值传递
        );
        //  tag-3、因为block定义的时候,只是对参数age进行值传递,所以block结构体内部生成【tag-2.1标记】的age值是独立存在的。
        // 那么这里修改变量的值,只是【tag-1标记】外边的值,影响不到block内部。
        age = 20;
        // tag-4、执行block的函数,这里面取的age的值,是block结构体内部的age值
        block->FuncPtr(block);
    }
    return 0;
}
// tag-2.1、block 结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age; // tag-2.1、生成一个新的值
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// tag-4、执行block的函数,这里面取的age的值,是block结构体内部的age值
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_vc_pn677_yj1sz8hgf_q5bjvssc0000gn_T_main_73f634_mi_0,age);
}


分析总结:


其逻辑结构图如下:

90da50cdafab42f288f2ec7e5a024ddb.png

1、刚开始 age = 10 的值,传递给了block内部的age【值传递】,它们之间没有绑定关系。

2、外部修改了age的值为20。

3、但是block内部的age变量,跟外部的age没有形成绑定关系。

4、综上,代码块内部的函数方法,取得值还是block内部的age值,打印结果还是为10。


总结:

修改 block 外部成员变量的值,是不会影响到 block 内部成员变量的值。


3、block的变量捕获(capture)


那么问题就来了,怎么让 block 内部能够正常访问外部的变量。

这里就涉及到了block变量捕获机制。


不多说,先来一张图,后面再分析各自的情况:


a232add6b1cb4f39b5d39e7241f118cc.png

1、捕获(capture):是指在 block 内部会专门新增一个成员变量,来存储传进来的值。或者说将外面的值捕获到 block 结构体中存储。

2、{ ... } :函数作用域,内部声明的变量都是局部变量。

3、auto:又称自动变量。其作用是离开对应的作用域该变量就会被销毁(值传递)。

(c语言中,当定义出一个局部变量的时候,会默认添加,所以不再需要手动添加)


如下面两个定义的变量是等价的:


auto int age = 0;
int age = 0;

注意:auto只存在于局部变量中。


4、static:静态变量,又分静态局部变量,静态全局变量


3.1、auto 捕获(capture)

auto 的捕获结果在第【2】大点上已经分析,这里不再阐述。


3.2、static 捕获(capture)

1、代码示例

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int age = 10;
        static int height = 10;
        void(^block)(void) = ^{
            NSLog(@"age:%d  height:%d",age,height);
        };
        age = 20;
        height = 20;
        block();
    }
    return 0;
}


// 打印结果

2022-05-07 16:58:09.938310+0800 block-01[56215:2690621] age:10  height:20

2、底层结构分析


int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        auto int age = 10;
        static int height = 10;
        void(*block)(void) = &__main_block_impl_0(
                __main_block_func_0,
                &__main_block_desc_0_DATA,
                age,//传递变量的值
                &height//传递变量的地址
                );
        age = 20;
        height = 20;
        block->FuncPtr(block);
    }
    return 0;
}
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // ------- 捕获到两个成员 ------------
  int age;// 该成员变量存储的是外部传进来的值(值传递)
  int *height;//该成员变量存储的是外部成员变量的地址值
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *height = __cself->height; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_vc_pn677_yj1sz8hgf_q5bjvssc0000gn_T_main_d0ee52_mi_0,
  age,//取block成员变量的值
  (*height)// 取出指针变量所指向内存的值
  );
}


分析总结:

1、只要是局部变量而且是 block 要访问的局部变量,block 都会将这些局部变量捕获到block结构体中。

2、捕获的区别就是 auto 进行的值传递,而 static 进行的是指针传递。


3.3、局部变量(auto、static)访问方式差异的原因分析

那么为什么会有这个差异呢?

1、auto 局部变量的内存随时可能被销毁,内存会消失。所以优先保证变量的值能拿到。

2、static 地址一直存在,可以在需要的时候取值,所以这里优先保证取得的数据最新。


举例如下:


#import <Foundation/Foundation.h>
// block 变量
void(^block)(void);
// 函数方法
void test() {
    // 1.0、执行完 test 方法,该成员变量就会被销毁
    auto int age = 10;
    // 2.0、由于 static 修饰的变量是一直在内存中的,即使test方法执行结束,这个变量还是会在内存中的
    static int height = 10;
    block = ^{
    // 此时 两个变量还能正常输出
    // 1.1、auto 自动变量是因为block内部存储了值(值传递),所以这里能正常取到值。yy:既然对方随时可能跑路,那么就赶紧把属于自己的先拿到。
    // 2.1、因为该成员变量的内存一直存在,所以这里可以直接取成员变量内存中的值。yy:既然对方都跑不了路,那就可以在需要的时候再去取,刚拿到的才是最新鲜的。
        NSLog(@"age:%d  height:%d",age,height);
    };
    age = 20;
    height = 20;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
      // 调用 block
        test();
        // 执行block
        block();
    }
    return 0;
}


4、全局变量访问


4.1、代码示例

//全局变量
int age_ = 10;
//全局静态变量
static int height_ = 10;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^{
            NSLog(@"age:%d  height:%d",age_,height_);
        };
        age_ = 20;
        height_ = 20;
        block();
            }
    return 0;
}


编译运行打印结果:


// 两个都能正常取到改变数值后的值
2022-05-07 20:33:10.697823+0800 block-01[59547:2776769] age:20  height:20

4.2、底层结构分析

int age_ = 10;
static int height_ = 10;
// 程序入口
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void(*block)(void) = &__main_block_impl_0(
                __main_block_func_0,
                &__main_block_desc_0_DATA));
        age_ = 20;
        height_ = 20;
        block->FuncPtr(block);
    }
    return 0;
}
// 在这里会发现,不管是全局变量还是全局静态变量,block都没有将这些变量捕获到结构体里面。
// 因为全局变量大家都可以访问,所以不需要去捕获
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __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;
  }
};
// 从这边的函数可以看到,是直接访问全局变量的
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_vc_pn677_yj1sz8hgf_q5bjvssc0000gn_T_main_ee8bdb_mi_0,
  age_,
  height_
  );
}
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)};


结论:

只要是全局变量,block内部不需要去捕获这些变量,可以直接进行访问。


5、self是否会捕获都 block 里面去?


5.1、示例代码

OC:


Person.h


#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic,assign) int age;
- (void)test;
@end

Person.m


#import "Person.h"
@implementation Person
- (void)test {
    void(^block)(void) = ^{
        NSLog(@"--------%p",self);
    };
    block();
}
@end


5.2、底层结构分析

/**
* 默认生成两个参数
* @param self:方法调用者本身,又称对象本身。因此self本身也是一个局部变量
* @param _cmd:这个方法的方法名称
*/
static void _I_Person_test(Person * self, SEL _cmd) {
    void(*block)(void) = &__Person__test_block_impl_0(
                __Person__test_block_func_0,
                &__Person__test_block_desc_0_DATA,
                self,
                570425344));
    block->FuncPtr(block);
}
struct __Person__test_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  Person *self;// self 成员变量
  __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


总结:


1、在OC中我们可以看到,当函数方法转换成C++时,都默认带有两个参数,第一个参数是调用者本身self,第二个参数是 _cmd 这个方法的方法名称。也就是说,OC函数方法是默认有这两个参数的,只是默认隐藏起来不显示出来而已。因此我们可以知道 self 是局部变量,不是全局变量, self 指向的是方法调用者本身。

2、正因为 self 是局部变量,所以 self 会捕获到 block 里面去。


相关文章
|
4月前
|
消息中间件 Kubernetes NoSQL
实现上下文保存和恢复的一种方式--setjmp与longjmp
实现上下文保存和恢复的一种方式--setjmp与longjmp
|
存储 固态存储 Linux
BLOCK 层这么多参数都是什么意思?!
每个 request queue 会维护一个 struct queue_limits 结构来描述对应的块设备的硬件参数,这些参数描述了硬件存储单元的组织方式,会影响 block layer 的很多行为,其中部分参数在 `/sys/block//queue/` 下导出 ```c struct request_queue { struct queue_limits limits; ... } `
3359 1
tp5写项目怎么屏蔽未定义变量的notice错误
tp5写项目怎么屏蔽未定义变量的notice错误
211 0