• 【Objective-C】浅析Block及其捕获机制


    Block的基本使用

    什么是Block?

    Block (块),封装了函数调用以及调用环境的 OC 对象Objective-C闭包(可以在内部访问外部的值),相当于C语言的函数指针,把一个函数写在一个函数内部,而OC并没有函数(方法)嵌套这一语法

    Block的声明

    void(^blockName)();
    int(^blockName2)(int a, int b, int c);
    
    • 1
    • 2

    格式: 返回值 (^block名称)(形参列表)
    ^代表块的符号

    Block的实现

    1. 无参数无返回值
    void(^blockName)(void) = ^{
    
    };
    
    • 1
    • 2
    • 3
    1. 有参数无返回值
    void(^blockName)(int a, int b) = ^(int a, int b){
    
    };
    
    • 1
    • 2
    • 3
    1. 无参数有返回值
    int(^blockName)(void) = ^int{
        return 3;
    };
    
    • 1
    • 2
    • 3

    实现部分的返回值可以省略,像这样:

    int(^blockName)(void) = ^{
        return 3;
    };
    
    • 1
    • 2
    • 3
    1. 有参数有返回值
    int(^blockName)(int a, int b) = ^int(int a, int b){
        return 3 + a * b;
    };
    
    • 1
    • 2
    • 3

    实现部分的返回值int同样可以省略

    Block的调用

    //无参数无返回值
    blockName();
    
    //有参数有返回值
    int result = blockName(7, 12);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    现在已声明的blockName代表一个块,那么调用这个块既可以通过blockName(7, 12);,也可以这样:

    int result = ^(int a, int b) {
        return 3 + a + b;
    }(7, 12);
    
    • 1
    • 2
    • 3

    Block作为形参使用

    Block作为形式参数在方法中的声明与上述格式略有不同(块的名称在外面)

    现在Jaxon类和Jacky类中分别实现以下方法:
    Jaxon.h

    - (void)askJackyForHelp: (void(^)(int num))blockName isOK: (void(^)(BOOL boolValue))completion;
    
    • 1

    Jaxon.m

    - (void)askJackyForHelp:(void (^)(int))blockName isOK:(void (^)(BOOL))completion {
        blockName(3);
    
        //传入completion块的参数非1即0
        completion(arc4random() % 2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Jacky.m

    - (void)helpDoWith: (int)num {
        NSLog(@"帮忙做事%d次", num);
    }
    
    • 1
    • 2
    • 3

    接下来在main函数中调用:

    Jaxon* jaxon = [[Jaxon alloc] init];
    [jaxon askJackyForHelp:^(int num) {
                Jacky* jacky = [[Jacky alloc] init];
                [jacky helpDoWith: num];
            } isOK:^(BOOL boolValue) {
    
                //成功和失败的概率各占一半
                if (boolValue) {
                    NSLog(@"帮忙成功");
                } else {
                    NSLog(@"帮忙失败");
                }
            }];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果:

    请添加图片描述

    这样是不是可以起到代理的作用,Jaxon委托Jacky帮忙做事,Jaxon实现不了的委托Jacky实现,因此Block块也可以用于界面传值或其他需要使用代理模式的程序设计中

    Block作为属性使用

    给Block起别名

    文章开头也提到了块其实也是一种对象,可以将ta理解为一种数据类型

    那么也可以用typedef关键字给Block起别名,看以下示例:

    typedef void(^Help)(int num);
    typedef void(^Finish)(BOOL boolValue);
    
    • 1
    • 2

    上面的方法也就可以这样声明:

    - (void)askJackyForHelp:(Help)blockName isOK:(Finish)completion;
    
    • 1

    块的属性关键字一般需要是是copy

    @interface Jaxon : NSObject
    
    //无别名
    @property (nonatomic, copy)void(^helpBlock)(int num);
    //有别名
    //@property (nonatomic, copy)Help helpBlock;
    - (void)askMyselfDo;
    
    @end
    
    @implementation Jaxon
    
    - (void)askMyselfDo {
        self.helpBlock(5);
    }
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    main函数:

    Jaxon* jaxon = [[Jaxon alloc] init];
    jaxon.helpBlock = ^(int num) {
        NSLog(@"我自己做%@次", @(num));
    };
    [jaxon askMyselfDo];
    
    • 1
    • 2
    • 3
    • 4
    • 5

    运行结果:
    请添加图片描述

    Block的copy

    关于copy关键字,编者也简单了解一下,底层原理以后再加以详细的剖析:

    ARC 环境下,编译器会根据情况自动将上的 block 复制到上,比如以下几种情况: 手动调用 block 的copy`方法时;

    • block 作为函数返回值时(Masonry 框架中用很多);
    • 将 block 赋值给__strong指针时;
    • block 作为 Cocoa API 中方法名含有usingBlock的方法参数时;
    • block 作为 GCD API 的方法参数时。

    block 作为属性的写法:
    ARC下写strong或者copy都会对 block 进行强引用,都会自动将 block 从栈 copy 到堆上;
    建议都写成copy,这样 MRC 和 ARC 下一致。

    Block刚创建时存放在栈区,使用时copy到堆区

    Block的捕获机制

    为保证Block内部能正常访问到外部的变量,Block有一种变量捕获机制

    auto类型的局部变量

    auto变量:正常定义出来的变量默认都是auto类型,只是省略了

    auto int age = 20;
    
    • 1

    auto类型的局部变量会被捕获到block块内部,访问方式为值传递

    int age = 10;
    NSLog(@"%d %p", age, &age);
    void(^blockName)(void) = ^ {
        NSLog(@"%d %p", age, &age);
    };
    age = 20;
    //可以打印出来,说明block块是可以访问到外部信息的
    blockName();
    NSLog(@"%d %p", age, &age);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    请添加图片描述
    根据运行结果可以得出以下两点:

    • auto类型的局部变量被捕获到block块内部时,block内部会自动生成一个相同的成员变量,用来存储这个变量的值,因此打印的block外部的age地址与内部age地址不一样
    • 由于值传递,修改外部age变量的值,不会影响到block内部的变量

    __block浅析

    Block内部只能调用外部变量,不能修改:

    请添加图片描述

    Block 默认情况下是使用被捕获的外部变量的只读拷贝,因此在 Block 内部无法直接修改外部变量的值

    解决办法如下:

    • 变量用static修饰(原因:捕获static类型的局部变量是指针传递,可以访问到该变量的内存地址)
    • 全局变量
    • __block(我们只希望临时用一下这个变量临时改一下而已,而改为 static 变量和全局变量会一直在内存中)

    当变量被__block修饰时,block可以修改外部全局变量:

    __block int age = 10;
    NSLog(@"%d %p", age, &age);
    void(^blockName)(void) = ^ {
        age = 30;
        NSLog(@"%d %p", age, &age);
    };
    blockName();
    NSLog(@"%d %p", age, &age);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    请添加图片描述


    static类型的局部变量

    static类型的局部变量会被捕获到block内部,访问方式指针传递

    static int age = 10;
    NSLog(@"%d %p", age, &age);
    void(^blockName)(void) = ^ {
        NSLog(@"%d %p", age, &age);
    };
    age = 20;
    blockName();
    NSLog(@"%d %p", age, &age);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    请添加图片描述

    • static类型的局部变量被捕获到block内部时,block块内部会生成一个相同类型的指针,指向捕获到内部的age变量的地址
    • 由于指针传递,修改外部的age变量的值,会影响到block内部的age变量

    全局变量

    全局变量不会被捕获到block内部,访问方式为直接访问

    int _age = 10;
    static int _height = 175;
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            NSLog(@"%d %p", _age, &_age);
            void(^blockName)(void) = ^ {
                _age = 30;
                NSLog(@"%d %p", _age, &_age);
            };
            blockName();
            _age = 20;
            NSLog(@"%d %p", _age, &_age);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    请添加图片描述

    其他问题

    对于对象类型的局部变量,block会连同ta的所有权修饰符一起捕获

    为什么局部变量需要捕获,全局变量不用捕获呢?

    • 作用域的原因,全局变量哪里都可以直接访问,所以不用捕获;
    • 局部变量,外部不能直接访问,所以需要捕获;
    • auto 类型的局部变量可能会销毁,其内存会消失,block 将来执行代码的时候不可能再去访问那块内存,所以捕获其值;
    • static 变量会一直保存在内存中, 所以捕获其地址即可
  • 相关阅读:
    【FPGA教程案例40】通信案例10——基于FPGA的简易OFDM系统verilog实现
    【Vue面试题三】、Vue中的v-show 和 v-if 怎么理解 ?
    Mybatis Plus如何使用自定义方法实现分页呢?
    docker基本概念与部署和基础命令
    【Java 进阶篇】JDBC数据库连接池Druid工具类详解
    HarmonyOS 3.1 第三方包导入
    Docker容器入门笔记,一篇文章告诉你,使用docker的方法和使用Docker的优势
    2.4 循环单链表:理论+C语言详细实现
    JavaScript中的四种枚举方式
    计算机毕业设计Java自由教学平台(源码+系统+mysql数据库+lw文档)
  • 原文地址:https://blog.csdn.net/XY_Mckevince/article/details/133918732