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

目录

Block的基本使用

什么是Block?

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

Block的声明

objectivec 复制代码
void(^blockName)();
int(^blockName2)(int a, int b, int c);

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

Block的实现

  1. 无参数无返回值
objectivec 复制代码
void(^blockName)(void) = ^{

};
  1. 有参数无返回值
objectivec 复制代码
void(^blockName)(int a, int b) = ^(int a, int b){

};
  1. 无参数有返回值
objectivec 复制代码
int(^blockName)(void) = ^int{
    return 3;
};

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

objectivec 复制代码
int(^blockName)(void) = ^{
    return 3;
};
  1. 有参数有返回值
objectivec 复制代码
int(^blockName)(int a, int b) = ^int(int a, int b){
    return 3 + a * b;
};

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

Block的调用

objectivec 复制代码
//无参数无返回值
blockName();

//有参数有返回值
int result = blockName(7, 12);

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

objectivec 复制代码
int result = ^(int a, int b) {
    return 3 + a + b;
}(7, 12);

Block作为形参使用

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

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

objectivec 复制代码
- (void)askJackyForHelp: (void(^)(int num))blockName isOK: (void(^)(BOOL boolValue))completion;

Jaxon.m

objectivec 复制代码
- (void)askJackyForHelp:(void (^)(int))blockName isOK:(void (^)(BOOL))completion {
    blockName(3);

    //传入completion块的参数非1即0
    completion(arc4random() % 2);
}

Jacky.m

objectivec 复制代码
- (void)helpDoWith: (int)num {
    NSLog(@"帮忙做事%d次", num);
}

接下来在main函数中调用:

objectivec 复制代码
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(@"帮忙失败");
            }
        }];

运行结果:

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

Block作为属性使用

给Block起别名

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

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

objectivec 复制代码
typedef void(^Help)(int num);
typedef void(^Finish)(BOOL boolValue);

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

objectivec 复制代码
- (void)askJackyForHelp:(Help)blockName isOK:(Finish)completion;

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

objectivec 复制代码
@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

main函数:

objectivec 复制代码
Jaxon* jaxon = [[Jaxon alloc] init];
jaxon.helpBlock = ^(int num) {
    NSLog(@"我自己做%@次", @(num));
};
[jaxon askMyselfDo];

运行结果:

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类型,只是省略了

objectivec 复制代码
auto int age = 20;

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

objectivec 复制代码
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);

根据运行结果可以得出以下两点:

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

__block浅析

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

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

解决办法如下:

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

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

objectivec 复制代码
__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);

static类型的局部变量

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

objectivec 复制代码
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);
  • static类型的局部变量被捕获到block内部时,block块内部会生成一个相同类型的指针,指向捕获到内部的age变量的地址
  • 由于指针传递,修改外部的age变量的值,会影响到block内部的age变量

全局变量

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

objectivec 复制代码
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;
}

其他问题

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

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

  • 作用域的原因,全局变量哪里都可以直接访问,所以不用捕获;
  • 局部变量,外部不能直接访问,所以需要捕获;
  • auto 类型的局部变量可能会销毁,其内存会消失,block 将来执行代码的时候不可能再去访问那块内存,所以捕获其值;
  • static 变量会一直保存在内存中, 所以捕获其地址即可
相关推荐
何曾参静谧4 分钟前
「QT」文件类 之 QTextStream 文本流类
开发语言·qt
monkey_meng8 分钟前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss16 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
2401_8532757337 分钟前
ArrayList 源码分析
java·开发语言
zyx没烦恼37 分钟前
【STL】set,multiset,map,multimap的介绍以及使用
开发语言·c++
lb363636363637 分钟前
整数储存形式(c基础)
c语言·开发语言
feifeikon40 分钟前
Python Day5 进阶语法(列表表达式/三元/断言/with-as/异常捕获/字符串方法/lambda函数
开发语言·python
大鲤余1 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust
浪里个浪的10241 小时前
【C语言】从3x5矩阵计算前三行平均值并扩展到4x5矩阵
c语言·开发语言·矩阵
MoFe11 小时前
【.net core】【sqlsugar】字符串拼接+内容去重
java·开发语言·.netcore