【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 变量会一直保存在内存中, 所以捕获其地址即可
相关推荐
用户0921 小时前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan21 小时前
iOS26适配指南之UIColor
ios·swift
权咚2 天前
阿权的开发经验小集
git·ios·xcode
用户092 天前
TipKit与CloudKit同步完全指南
ios·swift
小溪彼岸2 天前
macOS自带截图命令ScreenCapture
macos
法的空间2 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
侃侃_天下2 天前
最终的信号类
开发语言·c++·算法
echoarts2 天前
Rayon Rust中的数据并行库入门教程
开发语言·其他·算法·rust
2501_915918412 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
Aomnitrix2 天前
知识管理新范式——cpolar+Wiki.js打造企业级分布式知识库
开发语言·javascript·分布式