Objective-C中的块(Block)编程详解
一、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);
这里,将 3
和 5
作为参数传递给 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
类型的参数 a
和 b
,以及一个接受两个 int
参数并返回 int
类型结果的 Block operation
。addOperation
定义了加法操作,然后将其传递给 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);
};
在这个例子中,block
对 obj
进行了强引用。如果在 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);
在这个例子中,add
和 subtract
方法返回一个 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 作为观察回调。当 MyObservableObject
的 value
属性发生变化时,会执行 Block 中的代码。
通过以上对 Block 的详细介绍,从基本概念、语法、变量捕获、内存管理到与 GCD 的结合以及高级应用等方面,希望能帮助读者全面深入地理解 Objective - C 中的 Block 编程。在实际开发中,合理运用 Block 可以使代码更加简洁、灵活和高效。