ios中如何进行线程间通信

在 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_syncdispatch_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@synchronizedpthread_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. 使用 NSMachPortCFMessagePort

NSMachPortCFMessagePort 是基于 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);
}

注意事项:

  • NSMachPortCFMessagePort 是相对底层的机制,适合需要高性能或复杂通信的场景。

总结

在 iOS 开发中,线程间通信有多种方式,选择哪种方式取决于具体的需求:

  • 简单任务调度:使用 GCD(dispatch_async)是最简单和推荐的方式。
  • UI 更新:确保 UI 更新在主线程上执行,使用 dispatch_async(dispatch_get_main_queue(), ^{...})
  • 任务依赖:使用 NSOperationQueue 来管理任务依赖和取消。
  • 低级通信:如果需要更底层的线程间通信,可以使用 NSMachPortCFMessagePort
  • 通知机制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: 只能传递对象类型的参数。如果你需要传递非对象类型(如 intfloat 等),可以通过 NSNumberNSValue 进行包装。

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 通常是更好的选择。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容