MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的块(Block)编程详解

2021-01-177.4k 阅读

一、Block 的基本概念

在 Objective-C 中,块(Block)是一种带有自动变量(局部变量)的匿名函数。它可以捕获其定义范围内的局部变量,这使得它在很多场景下使用起来非常灵活。

从本质上讲,Block 是一个对象,它可以像函数一样被调用,而且能够访问定义它的词法作用域内的变量。这种特性在处理异步任务、回调函数等场景时极为有用。例如,在 iOS 开发中,网络请求完成后的回调处理、动画完成后的回调等,都经常会用到 Block。

二、Block 的语法

2.1 定义 Block

Block 的定义语法类似于函数指针,但有一些区别。其基本语法如下:

返回值类型 (^block名称)(参数列表) = ^返回值类型(参数列表) {
    // Block 的实现代码
};

这里,^ 符号用于标识 Block。例如,定义一个简单的加法 Block:

int (^addBlock)(int, int) = ^int(int a, int b) {
    return a + b;
};

在这个例子中,addBlock 是 Block 的名称,它接受两个 int 类型的参数,并返回一个 int 类型的值。^int(int a, int b) 部分定义了 Block 的参数列表和返回值类型,而大括号内是 Block 的具体实现。

也可以省略返回值类型的显式声明,编译器会根据 Block 内的 return 语句推断返回值类型。例如:

int (^addBlock)(int, int) = ^(int a, int b) {
    return a + b;
};

2.2 调用 Block

定义好 Block 后,调用它就像调用普通函数一样。例如,对于上面定义的 addBlock

int result = addBlock(3, 5);
NSLog(@"结果是: %d", result);

这里,将 35 作为参数传递给 addBlock,并将返回值赋给 result 变量,然后通过 NSLog 输出结果。

三、Block 捕获变量

3.1 捕获局部变量

Block 可以捕获定义它的词法作用域内的局部变量。例如:

int num = 10;
void (^printBlock)() = ^{
    NSLog(@"捕获的 num: %d", num);
};
printBlock();

在这个例子中,printBlock 捕获了局部变量 num,并在 Block 内部使用它。需要注意的是,Block 对捕获的局部变量是值拷贝。也就是说,如果在 Block 定义之后修改了 num 的值,Block 内部使用的仍然是捕获时 num 的值。例如:

int num = 10;
void (^printBlock)() = ^{
    NSLog(@"捕获的 num: %d", num);
};
num = 20;
printBlock();

上述代码输出的仍然是 捕获的 num: 10,因为 Block 捕获的是 num 的值,而不是 num 变量本身。

3.2 捕获 __block 修饰的局部变量

如果希望 Block 能够修改捕获的局部变量,可以使用 __block 关键字修饰该变量。例如:

__block int num = 10;
void (^updateBlock)() = ^{
    num = 20;
};
updateBlock();
NSLog(@"修改后的 num: %d", num);

在这个例子中,num__block 修饰,updateBlock 可以修改 num 的值。执行 updateBlock 后,num 的值变为 20,通过 NSLog 输出可以验证这一点。

3.3 捕获全局变量和静态变量

Block 可以直接访问全局变量和静态变量,并且对全局变量的修改会反映在全局作用域中。例如:

int globalNum = 10;
void (^modifyGlobalBlock)() = ^{
    globalNum = 20;
};
modifyGlobalBlock();
NSLog(@"全局变量 globalNum: %d", globalNum);

对于静态变量,情况类似:

void testStaticBlock() {
    static int staticNum = 10;
    void (^modifyStaticBlock)() = ^{
        staticNum = 20;
    };
    modifyStaticBlock();
    NSLog(@"静态变量 staticNum: %d", staticNum);
}

在上述代码中,modifyStaticBlock 可以修改静态变量 staticNum 的值。

四、Block 作为函数参数

4.1 简单的函数参数使用

在 Objective-C 中,经常会将 Block 作为函数参数传递,以实现回调功能。例如,定义一个函数,它接受一个 Block 作为参数,并在适当的时候调用这个 Block:

void executeBlock(void (^block)()) {
    block();
}
void (^printHelloBlock)() = ^{
    NSLog(@"Hello, Block!");
};
executeBlock(printHelloBlock);

在这个例子中,executeBlock 函数接受一个无参数无返回值的 Block 作为参数,并在函数内部调用这个 Block。printHelloBlock 定义了要执行的具体代码,然后将其传递给 executeBlock 函数。

4.2 带参数和返回值的 Block 作为参数

Block 作为函数参数也可以有参数和返回值。例如,定义一个函数,它接受两个整数和一个计算 Block,通过 Block 对这两个整数进行计算并返回结果:

int calculate(int a, int b, int (^operation)(int, int)) {
    return operation(a, b);
}
int (^addOperation)(int, int) = ^(int a, int b) {
    return a + b;
};
int result = calculate(3, 5, addOperation);
NSLog(@"计算结果: %d", result);

在这个例子中,calculate 函数接受两个 int 类型的参数 ab,以及一个接受两个 int 参数并返回 int 类型结果的 Block operationaddOperation 定义了加法操作,然后将其传递给 calculate 函数进行计算。

五、Block 作为函数返回值

5.1 返回简单 Block

函数也可以返回一个 Block。例如,定义一个函数,它根据传入的参数返回不同的 Block:

int (^createOperationBlock(BOOL isAddition)) {
    if (isAddition) {
        return ^(int a, int b) {
            return a + b;
        };
    } else {
        return ^(int a, int b) {
            return a - b;
        };
    }
}
int (^addBlock) = createOperationBlock(YES);
int (^subtractBlock) = createOperationBlock(NO);
int addResult = addBlock(3, 5);
int subtractResult = subtractBlock(3, 5);
NSLog(@"加法结果: %d", addResult);
NSLog(@"减法结果: %d", subtractResult);

在这个例子中,createOperationBlock 函数根据传入的 isAddition 参数返回加法或减法的 Block。然后分别调用返回的 Block 进行计算并输出结果。

5.2 返回捕获变量的 Block

返回的 Block 同样可以捕获函数内部的局部变量。例如:

int (^createMultiplicationBlock(int factor)) {
    return ^(int num) {
        return num * factor;
    };
}
int (^multiplyBy3Block) = createMultiplicationBlock(3);
int result = multiplyBy3Block(5);
NSLog(@"乘以 3 的结果: %d", result);

在这个例子中,createMultiplicationBlock 函数接受一个 factor 参数,并返回一个捕获了 factor 的 Block。这个 Block 将传入的 num 乘以 factor 并返回结果。

六、Block 的内存管理

6.1 Block 的存储类型

Block 在内存中有三种存储类型:栈上(NSStackBlock)、堆上(NSMallocBlock)和全局区(NSGlobalBlock)。

  • NSGlobalBlock:当 Block 没有捕获任何自动变量(局部变量)时,它存储在全局区。这种 Block 的生命周期和程序相同,不需要手动管理内存。例如:
void (^globalBlock)() = ^{
    NSLog(@"全局 Block");
};
  • NSStackBlock:默认情况下,Block 存储在栈上。栈上的 Block 生命周期和其定义所在的函数栈帧相同,当函数返回时,栈上的 Block 会被销毁。例如:
void testStackBlock() {
    int num = 10;
    void (^stackBlock)() = ^{
        NSLog(@"捕获的 num: %d", num);
    };
    stackBlock();
}
  • NSMallocBlock:如果希望 Block 的生命周期不受限于其定义所在的函数栈帧,可以将 Block 从栈上复制到堆上。通常通过调用 [block copy] 方法来实现。例如:
void testHeapBlock() {
    int num = 10;
    void (^stackBlock)() = ^{
        NSLog(@"捕获的 num: %d", num);
    };
    void (^heapBlock)() = [stackBlock copy];
    // 此时 heapBlock 存储在堆上,即使函数返回,它仍然存在
}

6.2 内存管理注意事项

在使用 Block 时,需要注意内存管理,特别是在涉及到对象捕获和循环引用的情况。

  • 对象捕获:当 Block 捕获对象类型的局部变量时,Block 会对该对象进行强引用。例如:
NSObject *obj = [[NSObject alloc] init];
void (^block)() = ^{
    NSLog(@"捕获的对象: %@", obj);
};

在这个例子中,blockobj 进行了强引用。如果在 Block 执行之前 obj 被释放,会导致野指针错误。

  • 循环引用:循环引用是使用 Block 时常见的问题。例如,在一个类的实例方法中,如果 Block 捕获了 self,而这个 Block 又被该实例对象持有,就会形成循环引用。例如:
@interface MyClass : NSObject
@property (nonatomic, copy) void (^block)();
- (void)setupBlock;
@end

@implementation MyClass
- (void)setupBlock {
    self.block = ^{
        NSLog(@"访问 self: %@", self);
    };
}
@end

在这个例子中,self.block 持有 Block,而 Block 又捕获了 self,形成了循环引用。为了解决这个问题,可以使用 __weak__unsafe_unretained 修饰符。例如,使用 __weak

@interface MyClass : NSObject
@property (nonatomic, copy) void (^block)();
- (void)setupBlock;
@end

@implementation MyClass
- (void)setupBlock {
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf) {
            NSLog(@"访问 self: %@", strongSelf);
        }
    };
}
@end

在这个改进的代码中,__weak typeof(self) weakSelf = self; 创建了一个对 self 的弱引用 weakSelf。在 Block 内部,通过 __strong typeof(self) strongSelf = weakSelf; 提升为强引用,这样在 Block 执行期间可以安全地访问 self,同时避免了循环引用。

七、Block 与 GCD(Grand Central Dispatch)

7.1 GCD 简介

GCD 是苹果公司开发的一种基于队列的高效异步编程模型,它可以极大地简化多线程编程。GCD 提供了全局队列和自定义队列,并且可以方便地将任务提交到队列中执行。

7.2 使用 Block 提交任务到 GCD 队列

在 GCD 中,任务通常以 Block 的形式提交到队列中。例如,将一个简单的任务提交到全局队列:

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    NSLog(@"在全局队列中执行任务");
});

在这个例子中,dispatch_get_global_queue 获取一个全局队列,dispatch_async 函数将一个 Block 任务提交到这个全局队列中异步执行。

对于自定义队列,可以使用 dispatch_queue_create 创建:

dispatch_queue_t customQueue = dispatch_queue_create("com.example.customQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(customQueue, ^{
    NSLog(@"在自定义串行队列中执行任务");
});

这里创建了一个名为 com.example.customQueue 的串行队列,并将任务提交到该队列中执行。

7.3 同步执行任务

除了异步执行任务(dispatch_async),GCD 还提供了同步执行任务的函数 dispatch_sync。例如:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"开始同步任务");
dispatch_sync(queue, ^{
    NSLog(@"在队列中同步执行任务");
});
NSLog(@"同步任务结束");

在这个例子中,dispatch_sync 会阻塞当前线程,直到提交的 Block 任务执行完毕。因此,输出结果会是先输出 开始同步任务,然后是 在队列中同步执行任务,最后是 同步任务结束

7.4 队列组(Dispatch Group)

队列组可以用于等待一组任务全部执行完毕。例如:

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{
    NSLog(@"任务 1 开始执行");
    // 模拟一些耗时操作
    sleep(2);
    NSLog(@"任务 1 执行完毕");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"任务 2 开始执行");
    // 模拟一些耗时操作
    sleep(1);
    NSLog(@"任务 2 执行完毕");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"所有任务执行完毕,回到主线程");
});

在这个例子中,dispatch_group_create 创建了一个队列组,dispatch_group_async 将任务提交到队列组中执行。dispatch_group_notify 用于在所有任务执行完毕后,将一个 Block 任务提交到指定的队列(这里是主线程队列)中执行。

八、Block 的高级应用

8.1 链式调用

通过巧妙地使用 Block,可以实现链式调用。例如,定义一个数学运算类,通过 Block 实现链式调用:

@interface MathChain : NSObject
@property (nonatomic, strong) NSNumber *result;
- (MathChain *(^)(NSNumber *))add;
- (MathChain *(^)(NSNumber *))subtract;
@end

@implementation MathChain
- (MathChain *(^)(NSNumber *))add {
    return ^(NSNumber *num) {
        self.result = @([self.result doubleValue] + [num doubleValue]);
        return self;
    };
}

- (MathChain *(^)(NSNumber *))subtract {
    return ^(NSNumber *num) {
        self.result = @([self.result doubleValue] - [num doubleValue]);
        return self;
    };
}
@end

使用时可以这样链式调用:

MathChain *mathChain = [[MathChain alloc] init];
mathChain.result = @0;
MathChain *resultChain = mathChain.add(@5).subtract(@3);
NSLog(@"最终结果: %@", resultChain.result);

在这个例子中,addsubtract 方法返回一个 Block,这个 Block 又返回 self,从而实现了链式调用。

8.2 与 KVO(Key - Value Observing)结合

Block 可以与 KVO 结合使用,提供更灵活的观察回调。例如:

@interface MyObservableObject : NSObject
@property (nonatomic, assign) int value;
@end

@implementation MyObservableObject
@end

@interface MyObserver : NSObject
@property (nonatomic, strong) MyObservableObject *observedObject;
- (void)startObserving;
@end

@implementation MyObserver
- (void)startObserving {
    __weak typeof(self) weakSelf = self;
    [self.observedObject addObserver:self forKeyPath:@"value" options:NSKeyValueObservingOptionNew context:NULL block:^(NSObservedObject * _Nonnull observed, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf) {
            NSLog(@"值发生变化: %d", [change[NSKeyValueChangeNewKey] intValue]);
        }
    }];
}

- (void)dealloc {
    [self.observedObject removeObserver:self forKeyPath:@"value"];
}
@end

在这个例子中,MyObserver 通过 addObserver:forKeyPath:options:context:block: 方法使用 Block 作为观察回调。当 MyObservableObjectvalue 属性发生变化时,会执行 Block 中的代码。

通过以上对 Block 的详细介绍,从基本概念、语法、变量捕获、内存管理到与 GCD 的结合以及高级应用等方面,希望能帮助读者全面深入地理解 Objective - C 中的 Block 编程。在实际开发中,合理运用 Block 可以使代码更加简洁、灵活和高效。