在 iOS 开发中,线程间通信是一个常见的需求。由于 UI 更新必须在主线程上执行,而耗时任务通常需要放在后台线程中处理,因此我们需要一种机制来在不同线程之间传递消息或数据。iOS 提供了多种方式进行线程间通信,以下是几种常见的方式:
1. 使用 performSelector:onThread:
performSelector:onThread:
是 Objective-C 中的一种跨线程通信方式,允许你在指定的线程上执行某个方法。
示例代码:
// 在后台线程中执行任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 模拟耗时任务
NSLog(@"后台线程处理任务");
// 在主线程上更新 UI
[self performSelector:@selector(updateUI)
onThread:[NSThread mainThread]
withObject:nil
waitUntilDone:NO];
});
// 在主线程上更新 UI
- (void)updateUI {
NSLog(@"在主线程上更新 UI");
// 更新 UI 的代码
}
注意事项:
- 目标线程必须有运行的 RunLoop。
- 只能传递一个对象类型的参数。
- 如果需要传递多个参数,可以使用
NSDictionary
或自定义对象。
2. 使用 GCD(Grand Central Dispatch)
GCD 是苹果推荐的多线程管理工具,提供了简洁的 API 来进行线程间的任务调度。GCD 可以轻松地将任务调度到主线程或后台线程。
2.1 调度到主线程
如果你在后台线程中处理了一些数据,并且需要在主线程上更新 UI,可以使用 dispatch_async
将任务调度到主线程。
// 在后台线程中执行任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 模拟耗时任务
NSLog(@"后台线程处理任务");
// 在主线程上更新 UI
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"在主线程上更新 UI");
// 更新 UI 的代码
});
});
2.2 调度到后台线程
你可以使用 dispatch_async
将任务调度到后台线程。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 在后台线程上执行任务
NSLog(@"后台线程执行任务");
});
2.3 使用 dispatch_sync
和 dispatch_async
-
dispatch_async
: 异步执行任务,不会阻塞当前线程。 -
dispatch_sync
: 同步执行任务,会阻塞当前线程,直到任务完成。
// 异步执行任务
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"异步执行任务");
});
// 同步执行任务
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"同步执行任务");
});
注意事项:
- 避免在主线程上调用
dispatch_sync
,否则可能会导致死锁。
3. 使用 NSOperationQueue
NSOperationQueue
是另一种高级的多线程管理工具,它是基于 GCD 的封装,提供了更多的控制和灵活性。
示例代码:
// 创建一个操作队列
NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
// 在后台线程中执行任务
[backgroundQueue addOperationWithBlock:^{
NSLog(@"后台线程处理任务");
// 在主线程上更新 UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"在主线程上更新 UI");
// 更新 UI 的代码
}];
}];
注意事项:
-
NSOperationQueue
提供了更多的功能,例如任务依赖、取消任务等。 - 主线程的操作队列可以通过
[NSOperationQueue mainQueue]
获取。
4. 使用 NotificationCenter
NSNotificationCenter
可以用于在同一个进程内的不同线程之间传递消息。虽然它本身不支持跨线程通信,但你可以结合 performSelector:onThread:
或 GCD 来实现跨线程通信。
示例代码:
// 注册通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotification:)
name:@"MyNotification"
object:nil];
// 在后台线程中发送通知
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"MyNotification" object:nil];
});
// 处理通知
- (void)handleNotification:(NSNotification *)notification {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"在主线程中处理通知: %@", notification);
});
}
注意事项:
-
NSNotificationCenter
默认是线程安全的,但通知的处理逻辑可能不是。确保通知的处理逻辑在正确的线程上执行。
5. 使用信号量(Semaphore)
信号量是一种同步机制,可以用来协调线程之间的执行顺序。dispatch_semaphore_t
是 GCD 提供的信号量类型。
示例代码:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// 在后台线程中执行任务
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"后台线程处理任务");
// 任务完成后释放信号量
dispatch_semaphore_signal(semaphore);
});
// 等待信号量
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"任务完成,继续执行");
注意事项:
- 信号量可以用于线程同步,但要小心避免死锁。
6. 使用锁(Locks)
在多线程环境中,共享资源的访问需要加锁以避免竞争条件。iOS 提供了多种锁机制,如 NSLock
、@synchronized
、pthread_mutex
等。
示例代码:
NSLock *lock = [[NSLock alloc] init];
// 线程 1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
NSLog(@"线程 1 锁定资源");
// 访问共享资源
[lock unlock];
});
// 线程 2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
NSLog(@"线程 2 锁定资源");
// 访问共享资源
[lock unlock];
});
注意事项:
- 加锁会影响性能,尽量减少锁的范围。
- 避免死锁,确保锁的获取和释放顺序一致。
7. 使用 Block 回调
你可以在后台线程中执行任务,并通过 Block 回调将结果传递回主线程。
示例代码:
// 定义一个带有回调的函数
- (void)doBackgroundTaskWithCompletion:(void (^)(NSString *))completion {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 模拟耗时任务
NSString *result = @"后台任务完成";
// 回调到主线程
dispatch_async(dispatch_get_main_queue(), ^{
completion(result);
});
});
}
// 调用带有回调的任务
[self doBackgroundTaskWithCompletion:^(NSString *result) {
NSLog(@"任务完成,结果: %@", result);
}];
注意事项:
- Block 回调是一种非常灵活的方式,适用于简单的任务结果传递。
8. 使用 NSMachPort
或 CFMessagePort
NSMachPort
和 CFMessagePort
是基于 Mach Port 的低级线程间通信机制,适用于更复杂的线程间通信场景。
示例代码(NSMachPort):
// 创建 Mach Port
NSMachPort *machPort = [NSMachPort port];
machPort.delegate = self;
// 将 Mach Port 添加到 RunLoop
[[NSRunLoop currentRunLoop] addPort:machPort forMode:NSDefaultRunLoopMode];
// 发送消息
struct {
mach_msg_header_t header;
} message;
message.header.msgh_remote_port = (mach_port_t)[machPort machPort];
message.header.msgh_local_port = MACH_PORT_NULL;
message.header.msgh_id = 12345;
kern_return_t result = mach_msg(&message.header,
MACH_SEND_MSG,
sizeof(message),
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (result != KERN_SUCCESS) {
NSLog(@"消息发送失败: %s", mach_error_string(result));
}
// 处理消息
- (void)handleMachMessage:(void *)msg {
mach_msg_header_t *header = (mach_msg_header_t *)msg;
NSLog(@"接收到 Mach 消息,消息 ID: %d", header->msgh_id);
}
注意事项:
-
NSMachPort
和CFMessagePort
是相对底层的机制,适合需要高性能或复杂通信的场景。
总结
在 iOS 开发中,线程间通信有多种方式,选择哪种方式取决于具体的需求:
-
简单任务调度:使用 GCD(
dispatch_async
)是最简单和推荐的方式。 -
UI 更新:确保 UI 更新在主线程上执行,使用
dispatch_async(dispatch_get_main_queue(), ^{...})
。 -
任务依赖:使用
NSOperationQueue
来管理任务依赖和取消。 -
低级通信:如果需要更底层的线程间通信,可以使用
NSMachPort
或CFMessagePort
。 -
通知机制:
NSNotificationCenter
可以用于同一进程内的线程间通信。
每种方式都有其适用的场景,开发者应根据实际需求选择合适的线程间通信方式。
performSelector:onThread:
performSelector:onThread:
是 Objective-C 中的一个方法,用于在指定的线程上执行某个选择器(selector)。它允许你将某个方法调用分发到特定的线程上执行,通常用于跨线程通信或确保某些代码在特定线程(例如主线程)上运行。
1. performSelector:onThread:
的基本语法
- (void)performSelector:(SEL)aSelector
onThread:(NSThread *)thread
withObject:(id)arg
waitUntilDone:(BOOL)wait;
参数说明:
aSelector: 要执行的方法的选择器(selector),该方法必须接受一个参数并返回
void
。thread: 指定要在哪个线程上执行该方法。通常你会传入主线程或其他后台线程。
arg: 传递给目标方法的参数。如果不需要传递参数,可以传入
nil
。wait: 是否等待当前线程阻塞,直到目标方法执行完毕。如果为
YES
,当前线程会阻塞,直到目标方法执行完成;如果为NO
,当前线程不会等待,目标方法会在指定线程上异步执行。
2. 使用场景
performSelector:onThread:
主要用于以下场景:
跨线程通信:当你需要在不同的线程之间传递消息或执行某些操作时,可以使用
performSelector:onThread:
来确保代码在正确的线程上执行。确保主线程执行:在 iOS 开发中,UI 更新必须在主线程上执行。如果你在后台线程中处理了一些数据,并且需要更新 UI,可以使用
performSelector:onThread:
将 UI 更新操作调度到主线程。
3. 示例代码
3.1 在主线程上执行方法
假设你在一个后台线程中处理了一些数据,并且需要在主线程上更新 UI。你可以使用 performSelector:onThread:
将 UI 更新操作调度到主线程。
// 假设这是在后台线程中执行的代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 处理一些耗时任务
NSLog(@"后台线程处理任务");
// 在主线程上更新 UI
[self performSelector:@selector(updateUI)
onThread:[NSThread mainThread]
withObject:nil
waitUntilDone:NO];
});
// 在主线程上更新 UI
- (void)updateUI {
NSLog(@"在主线程上更新 UI");
// 更新 UI 的代码
}
在这个例子中,updateUI
方法会被调度到主线程上执行,确保 UI 更新操作在主线程上进行。
3.2 在自定义线程上执行方法
你也可以在自定义的线程上执行方法。首先,你需要创建一个新的线程,并确保该线程的 RunLoop 正在运行。
// 创建一个新的线程
NSThread *customThread = [[NSThread alloc] initWithTarget:self
selector:@selector(threadMainMethod)
object:nil];
[customThread start];
// 线程的主方法
- (void)threadMainMethod {
@autoreleasepool {
// 启动 RunLoop
[[NSRunLoop currentRunLoop] run];
}
}
// 在后台线程中发送通知
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 在自定义线程上执行方法
[self performSelector:@selector(doSomething)
onThread:customThread
withObject:nil
waitUntilDone:NO];
});
// 自定义线程上执行的任务
- (void)doSomething {
NSLog(@"在自定义线程上执行任务");
}
在这个例子中,我们创建了一个自定义线程,并确保它的 RunLoop 正在运行。然后,我们使用 performSelector:onThread:
将 doSomething
方法调度到这个自定义线程上执行。
4. 注意事项
4.1 必须有 RunLoop
performSelector:onThread:
要求目标线程的 RunLoop 正在运行,否则方法不会被执行。因此,在使用 performSelector:onThread:
之前,确保目标线程的 RunLoop 已经启动。
[[NSRunLoop currentRunLoop] run];
4.2 只能传递一个参数
performSelector:onThread:
只能传递一个参数。如果你需要传递多个参数,可以考虑使用 NSDictionary
或自定义对象来封装多个参数。
NSDictionary *userInfo = @{@"key1": @"value1", @"key2": @"value2"};
[self performSelector:@selector(handleNotification:)
onThread:[NSThread mainThread]
withObject:userInfo
waitUntilDone:NO];
- (void)handleNotification:(NSDictionary *)userInfo {
NSLog(@"接收到通知: %@", userInfo);
}
4.3 不支持 ARC 下的非对象类型
在 ARC(自动引用计数)环境下,performSelector:onThread:
只能传递对象类型的参数。如果你需要传递非对象类型(如 int
、float
等),可以通过 NSNumber
或 NSValue
进行包装。
NSNumber *number = @(42);
[self performSelector:@selector(handleNumber:)
onThread:[NSThread mainThread]
withObject:number
waitUntilDone:NO];
- (void)handleNumber:(NSNumber *)number {
NSLog(@"接收到数字: %d", [number intValue]);
}
4.4 避免死锁
如果你将 waitUntilDone
设置为 YES
,当前线程会阻塞,直到目标线程上的方法执行完毕。如果目标线程正在等待当前线程完成某些操作,可能会导致死锁。因此,谨慎使用 waitUntilDone:YES
。
5. 替代方案
虽然 performSelector:onThread:
是一种非常方便的方式,但在现代开发中,GCD(Grand Central Dispatch)通常是更推荐的跨线程通信方式。GCD 提供了更简洁和灵活的 API 来处理多线程任务。
使用 GCD 调度到主线程
dispatch_async(dispatch_get_main_queue(), ^{
// 在主线程上执行任务
[self updateUI];
});
使用 GCD 调度到后台线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 在后台线程上执行任务
[self doSomething];
});
6. 总结
performSelector:onThread:
是一种在指定线程上执行方法的机制,适用于跨线程通信或确保某些代码在特定线程上执行。使用场景:主要用于跨线程通信,尤其是确保 UI 更新操作在主线程上执行。
注意事项:目标线程必须有运行的 RunLoop,且只能传递一个对象类型的参数。
替代方案:在现代开发中,GCD 是更推荐的跨线程通信方式,提供了更简洁和灵活的 API。
总之,performSelector:onThread:
是一种有效的跨线程通信工具,但在现代开发中,GCD 通常是更好的选择。