Objective-C 中 Block 的使用与高级技巧
一、Block 基础概念
在 Objective - C 中,Block 是一种带有自动变量(局部变量)的匿名函数。它可以作为一个值进行传递,存储在变量中,或者作为参数传递给其他函数,甚至可以从函数中返回。
1.1 Block 的定义与语法
Block 的基本语法形式如下:
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {
// 执行代码块
statements;
return value;
};
其中,returnType
是 Block 的返回值类型,blockName
是 Block 的名称,parameterTypes
和 parameters
分别是 Block 的参数类型和参数列表。如果 Block 没有返回值,returnType
可以省略,或者写为 void
;如果 Block 没有参数,(parameterTypes)
和 (parameters)
可以省略。
例如,定义一个简单的 Block,接受两个整数并返回它们的和:
int (^sumBlock)(int, int) = ^int(int a, int b) {
return a + b;
};
这里定义了一个名为 sumBlock
的 Block,它接受两个 int
类型的参数,并返回一个 int
类型的值。可以通过以下方式调用这个 Block:
int result = sumBlock(3, 5);
NSLog(@"The sum is %d", result);
上述代码会输出 The sum is 8
。
1.2 Block 与函数指针的区别
虽然 Block 看起来有点像函数指针,但它们有本质的区别。函数指针指向一个函数的入口地址,而 Block 不仅包含代码,还可以捕获其定义时所在作用域中的自动变量(局部变量)。
例如:
int num = 10;
void (^printBlock)() = ^{
NSLog(@"The number is %d", num);
};
num = 20;
printBlock();
在上述代码中,printBlock
在定义时捕获了 num
变量。尽管之后 num
的值被修改为 20
,但当调用 printBlock
时,输出的仍然是 10
,这是因为 Block 捕获的是变量的副本。
二、Block 的使用场景
2.1 作为回调函数
在很多异步操作中,如网络请求、文件读取等,Block 常被用作回调函数。例如,在 iOS 开发中使用 NSURLSession
进行网络请求时:
NSURL *url = [NSURL URLWithString:@"https://example.com"];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (!error && data) {
// 处理响应数据
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"Response: %@", responseString);
} else {
NSLog(@"Error: %@", error);
}
}];
[task resume];
在这个例子中,completionHandler
是一个 Block,当网络请求完成时,系统会调用这个 Block,并将请求得到的数据、响应以及可能出现的错误作为参数传递进来。
2.2 集合遍历
在处理集合(如 NSArray
、NSDictionary
)时,Block 提供了一种简洁的遍历方式。
对于 NSArray
:
NSArray *numbers = @[@1, @2, @3, @4, @5];
[numbers enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"Index %lu: %@", (unsigned long)idx, obj);
if (idx == 2) {
*stop = YES;
}
}];
上述代码使用 enumerateObjectsUsingBlock:
方法遍历 numbers
数组。在 Block 中,obj
是当前遍历到的对象,idx
是对象的索引,stop
是一个指针,通过设置 *stop = YES
可以提前终止遍历。
对于 NSDictionary
:
NSDictionary *dict = @{@"key1": @"value1", @"key2": @"value2", @"key3": @"value3"};
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"Key: %@, Value: %@", key, obj);
}];
这里使用 enumerateKeysAndObjectsUsingBlock:
方法遍历字典,key
是字典的键,obj
是对应的值。
三、Block 的内存管理
3.1 Block 的存储域
在 Objective - C 中,Block 有三种存储域:栈(Stack)、堆(Heap)和全局区(Global)。
- 栈 Block:当 Block 在函数内部定义,且没有被 copy 操作时,它存储在栈上。栈 Block 的生命周期与函数栈帧相同,函数结束时,栈 Block 会被释放。例如:
void testStackBlock() {
int num = 10;
void (^stackBlock)() = ^{
NSLog(@"The number is %d", num);
};
stackBlock();
}
在这个例子中,stackBlock
是一个栈 Block,它在 testStackBlock
函数内部定义,并且没有被 copy。
- 堆 Block:通过
copy
操作,栈 Block 会被复制到堆上,成为堆 Block。堆 Block 的生命周期由引用计数管理,当引用计数为 0 时,堆 Block 会被释放。例如:
void testHeapBlock() {
int num = 10;
void (^stackBlock)() = ^{
NSLog(@"The number is %d", num);
};
void (^heapBlock)() = [stackBlock copy];
stackBlock = nil;
heapBlock();
}
这里,stackBlock
是栈 Block,通过 copy
操作创建了 heapBlock
,heapBlock
是堆 Block。即使 stackBlock
被置为 nil
,heapBlock
仍然可以正常调用。
- 全局 Block:当 Block 不捕获任何自动变量(局部变量)时,它存储在全局区。全局 Block 的生命周期与应用程序相同。例如:
void (^globalBlock)() = ^{
NSLog(@"This is a global block");
};
globalBlock
没有捕获任何局部变量,所以它是全局 Block。
3.2 Block 对对象的引用
当 Block 捕获对象类型的自动变量时,会对该对象产生强引用。例如:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
@end
void testBlockObjectCapture() {
Person *person = [[Person alloc] init];
person.name = @"John";
void (^block)() = ^{
NSLog(@"The person's name is %@", person.name);
};
block();
person = nil;
block();
}
在这个例子中,block
捕获了 person
对象,对 person
产生了强引用。即使在 person = nil
之后,block
仍然可以正常访问 person
的属性,因为 block
持有 person
的引用。
为了避免循环引用(如在 self
和 Block 之间),可以使用 __weak
修饰符。例如:
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
strongSelf.title = @"New Title";
}
});
}
@end
这里,首先使用 __weak
修饰符创建了一个弱引用 weakSelf
,在 Block 内部,通过 __strong
修饰符将弱引用提升为强引用 strongSelf
,这样可以确保在 Block 执行期间 self
不会被释放,同时避免了循环引用。
四、Block 的高级技巧
4.1 Block 作为函数参数的变化
在 Objective - C 中,Block 作为函数参数时,其声明语法有一些变化。例如,定义一个函数,接受一个 Block 作为参数:
void executeBlock(void (^block)()) {
block();
}
这里,executeBlock
函数接受一个没有参数和返回值的 Block。调用这个函数的方式如下:
void (^myBlock)() = ^{
NSLog(@"This is my block");
};
executeBlock(myBlock);
如果 Block 有参数和返回值,函数声明会相应变化。例如,接受一个接受两个整数并返回它们乘积的 Block:
int executeMultiplyBlock(int (^block)(int, int), int a, int b) {
return block(a, b);
}
调用这个函数:
int (^multiplyBlock)(int, int) = ^int(int a, int b) {
return a * b;
};
int result = executeMultiplyBlock(multiplyBlock, 3, 5);
NSLog(@"The product is %d", result);
4.2 Block 与协议
可以在协议中定义 Block 类型的方法。例如:
@protocol MyProtocol <NSObject>
- (void)performActionWithBlock:(void (^)(void))block;
@end
@interface MyClass : NSObject <MyProtocol>
@end
@implementation MyClass
- (void)performActionWithBlock:(void (^)(void))block {
block();
}
@end
在这个例子中,MyProtocol
协议定义了一个接受 Block 作为参数的方法 performActionWithBlock:
。MyClass
实现了这个协议,并在实现中调用了传入的 Block。
4.3 递归 Block
Block 也可以实现递归。例如,计算阶乘的递归 Block:
NSInteger (^factorialBlock)(NSInteger) = ^NSInteger(NSInteger num) {
if (num <= 1) {
return 1;
} else {
return num * factorialBlock(num - 1);
}
};
NSInteger result = factorialBlock(5);
NSLog(@"The factorial of 5 is %ld", (long)result);
这里,factorialBlock
是一个递归 Block,它在自身内部调用自己来计算阶乘。
4.4 可变参数 Block
在某些情况下,可能需要定义接受可变参数的 Block。虽然 Objective - C 本身没有直接支持可变参数 Block 的语法,但可以通过 va_list
来实现类似功能。例如:
void (^printNumbersBlock)(...) = ^(va_list args) {
while (true) {
NSNumber *number = va_arg(args, NSNumber *);
if (!number) {
break;
}
NSLog(@"%@", number);
}
};
va_list args;
va_start(args, nil);
va_arg(args, NSNumber *);
va_arg(args, NSNumber *);
va_end(args);
printNumbersBlock(args);
在这个例子中,printNumbersBlock
是一个通过 va_list
模拟可变参数的 Block。首先通过 va_start
初始化 va_list
,然后使用 va_arg
逐个获取参数,最后通过 va_end
结束。
五、Block 与 GCD(Grand Central Dispatch)
5.1 GCD 简介
GCD 是 Apple 开发的一种基于队列的高效异步编程模型,它在多核处理器上可以充分利用硬件资源,提高应用程序的性能。GCD 中的任务以 Block 的形式提交到队列中执行。
5.2 队列与 Block
GCD 中有两种类型的队列:串行队列和并发队列。
- 串行队列:任务按照提交的顺序依次执行。可以通过
dispatch_queue_create
创建自定义串行队列:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
// 任务 1
NSLog(@"Task 1 in serial queue");
});
dispatch_async(serialQueue, ^{
// 任务 2
NSLog(@"Task 2 in serial queue");
});
在这个例子中,Task 1
会先执行,然后 Task 2
执行。
- 并发队列:任务可以并发执行,但仍然按照提交的顺序开始执行。系统提供了全局并发队列,可以通过
dispatch_get_global_queue
获取:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
// 任务 1
NSLog(@"Task 1 in global queue");
});
dispatch_async(globalQueue, ^{
// 任务 2
NSLog(@"Task 2 in global queue");
});
这里,Task 1
和 Task 2
可能会并发执行,具体取决于系统资源和调度。
5.3 同步与异步执行
在 GCD 中,可以使用 dispatch_sync
和 dispatch_async
来提交任务。
- 异步执行(
dispatch_async
):将任务提交到队列后,立即返回,不会阻塞当前线程。例如:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
// 异步任务
NSLog(@"Asynchronous task");
});
NSLog(@"Main thread continues");
在这个例子中,dispatch_async
将任务提交到全局队列后,主线程继续执行,NSLog(@"Main thread continues")
会先输出,然后异步任务中的 NSLog(@"Asynchronous task")
输出。
- 同步执行(
dispatch_sync
):将任务提交到队列后,会等待任务执行完毕才返回,会阻塞当前线程。例如:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{
// 同步任务
NSLog(@"Synchronous task");
});
NSLog(@"Main thread continues");
这里,dispatch_sync
会阻塞主线程,直到同步任务执行完毕,所以 NSLog(@"Synchronous task")
会先输出,然后 NSLog(@"Main thread continues")
输出。
5.4 队列组(Dispatch Group)
队列组可以用来管理一组任务,当所有任务执行完毕后执行特定操作。例如:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
// 任务 1
NSLog(@"Task 1 in group");
});
dispatch_group_async(group, queue, ^{
// 任务 2
NSLog(@"Task 2 in group");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 所有任务完成后的操作
NSLog(@"All tasks in group are completed");
});
在这个例子中,dispatch_group_async
将任务提交到队列并加入队列组,dispatch_group_notify
会在所有任务完成后,在主线程执行指定的操作。
六、Block 与多线程
6.1 Block 与线程安全
在多线程环境下使用 Block 时,需要注意线程安全问题。如果多个线程同时访问和修改 Block 捕获的共享资源,可能会导致数据竞争和不一致。
例如,假设多个线程同时调用一个 Block,该 Block 对一个共享变量进行自增操作:
int sharedVariable = 0;
void (^incrementBlock)() = ^{
sharedVariable++;
};
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(concurrentQueue, incrementBlock);
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Shared variable value: %d", sharedVariable);
});
在上述代码中,如果不采取任何线程同步措施,最终 sharedVariable
的值可能不是 1000,因为多个线程同时访问和修改它会导致数据竞争。
6.2 使用锁保证线程安全
为了保证线程安全,可以使用锁来保护共享资源。例如,使用 @synchronized
关键字:
int sharedVariable = 0;
void (^incrementBlock)() = ^{
@synchronized(self) {
sharedVariable++;
}
};
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(concurrentQueue, incrementBlock);
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Shared variable value: %d", sharedVariable);
});
在这个例子中,@synchronized(self)
确保了在同一时间只有一个线程可以进入代码块修改 sharedVariable
,从而保证了线程安全。
除了 @synchronized
,还可以使用 NSLock
、pthread_mutex
等其他锁机制来保证线程安全。
七、Block 的性能优化
7.1 减少 Block 的创建开销
Block 的创建和销毁都有一定的开销。在性能敏感的代码中,应尽量减少不必要的 Block 创建。例如,如果一个 Block 在循环中被多次创建,可以将其提取到循环外部。
例如,原本在循环中创建 Block:
for (int i = 0; i < 1000; i++) {
void (^block)() = ^{
NSLog(@"Iteration %d", i);
};
block();
}
优化后,将 Block 提取到循环外部:
void (^block)() = ^{
NSLog(@"Iteration %d", i);
};
for (int i = 0; i < 1000; i++) {
block();
}
这样可以减少 Block 的创建开销,提高性能。
7.2 避免循环引用导致的内存泄漏
如前文所述,循环引用会导致内存泄漏,从而影响性能。通过使用 __weak
修饰符来打破循环引用是非常重要的。在使用 Block 时,尤其是在对象的属性中使用 Block,要仔细检查是否存在循环引用的可能。
例如,在一个视图控制器中,如果存在以下情况:
@interface ViewController : UIViewController
@property (nonatomic, copy) void (^block)();
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.block = ^{
NSLog(@"View controller title: %@", self.title);
};
}
@end
这里就存在 self
和 block
之间的循环引用。应该使用 __weak
修饰符来打破循环:
@interface ViewController : UIViewController
@property (nonatomic, copy) void (^block)();
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"View controller title: %@", strongSelf.title);
}
};
}
@end
这样可以避免因循环引用导致的内存泄漏,提高应用程序的性能。
7.3 合理使用 GCD 队列
在使用 Block 与 GCD 时,合理选择队列类型对性能有很大影响。对于 I/O 密集型任务,通常使用并发队列可以提高效率,因为 I/O 操作往往需要等待外部设备响应,并发执行可以充分利用等待时间。而对于 CPU 密集型任务,如果任务之间相互独立,可以考虑使用并发队列;如果任务之间存在依赖关系,可能需要使用串行队列。
例如,在进行文件读取操作时:
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
NSData *data = [NSData dataWithContentsOfFile:@"path/to/file"];
if (data) {
// 处理数据
}
});
这里使用并发队列可以在文件读取等待时执行其他任务,提高整体性能。
八、总结 Block 的最佳实践
- 明确 Block 的作用:在使用 Block 之前,清楚它是作为回调函数、遍历集合还是用于其他目的,这样可以更好地设计 Block 的参数和返回值。
- 注意内存管理:了解 Block 的存储域,避免循环引用,尤其是在涉及对象引用时。使用
__weak
修饰符来打破循环引用,确保内存的正确释放。 - 考虑性能优化:减少不必要的 Block 创建,合理选择 GCD 队列以提高性能。在多线程环境下,确保 Block 访问共享资源的线程安全。
- 保持代码清晰:Block 的语法虽然灵活,但也要注意保持代码的可读性。给 Block 命名时,尽量使用有意义的名称,并且在复杂的 Block 内部添加注释。
通过遵循这些最佳实践,可以更好地在 Objective - C 项目中使用 Block,提高代码的质量和性能。
Block 作为 Objective - C 中强大的特性,为开发者提供了灵活、高效的编程方式。深入理解和掌握 Block 的使用与高级技巧,对于开发高质量的 iOS 和 macOS 应用程序至关重要。无论是异步编程、集合操作还是多线程处理,Block 都发挥着重要的作用。希望通过本文的介绍,读者能够更加熟练地运用 Block 来解决实际开发中的问题。