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

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

✅作者简介:大家好我是瓜子三百克,一个非科班出身的技术程序员,还是喜欢在学习和开发中记录笔记的博主小白!
📃个人主页:瓜子三百克的主页
🔥系列专栏:OC语法
🤟格言:作为一个程序员都应该认识到,好的代码是初级工程师都可以理解的代码, 伟大的代码是可以被学习一年计算机专业的新生所理解。
💖如果觉得博主的文章还不错的话,请点赞👍+收藏⭐️+留言📝支持一下博主哦🤞


让我们一起卷起来吧!!!


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

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

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、代码示例
    • 1.2、代码转成C++
    • 1.3、底层结构分析
  • 2、block内部引用局部变量
    • 2.1、代码示例
    • 2.2、底层结构分析
  • 3、block的变量捕获(capture)
    • 3.1、auto 捕获(capture)
    • 3.2、static 捕获(capture)
      • 1、代码示例
      • 2、底层结构分析
    • 3.3、局部变量(auto、static)访问方式差异的原因分析
  • 4、全局变量访问
    • 4.1、代码示例
    • 4.2、底层结构分析
  • 5、self是否会捕获都 block 里面去?
    • 5.1、示例代码
    • 5.2、底层结构分析

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);
}

分析总结:

其逻辑结构图如下:
1、刚开始 age = 10 的值,传递给了block内部的age【值传递】,它们之间没有绑定关系。
2、外部修改了age的值为20。
3、但是block内部的age变量,跟外部的age没有形成绑定关系。
4、综上,代码块内部的函数方法,取得值还是block内部的age值,打印结果还是为10。

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

3、block的变量捕获(capture)

那么问题就来了,怎么让 block 内部能够正常访问外部的变量。
这里就涉及到了block变量捕获机制。

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


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 里面去。


**🏆结束语🏆 **

最后如果觉得我写的文章对您有帮助的话,欢迎点赞✌,收藏✌,加关注✌哦,谢谢谢谢!!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/872613.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-13
下一篇 2022-05-13

发表评论

登录后才能评论

评论列表(0条)

保存