线程间通信能不用就不用
好像两辆行驶的车之间交换内容
用performSelector静好就是晴天
一、了解NSPort
- NSPort是描述通信通道的抽象类。在两个NSPort对象之间通过
- (BOOL)sendBeforeDate:(NSDate *)limitDate msgid:(NSUInteger)msgID components:(nullable NSMutableArray *)components from:(nullable NSPort *)receivePort reserved:(NSUInteger)headerSpaceReserved;
发送消息和handlePortMessage:
代理接受NSPortMessage对象,来实现线程(或进程或应用)之间的通信。 - NSPort对象必须作为输入源添加到NSRunLoop对象中,来实现线程不退出。
- NSPort有3个子类NSMachPort、NSMessagePort和NSSocketPort。
- CFRunLoopSource中source1事件源采用port,来监听系统端口和通过内核和其他线程发送消息。
二、线程间通信思路
- 在主线程创建一个NSPort的实例A并添加主线程的NSRunLoop中。
- 再创建一个线程S将A作为参数发送到线程中。
- 线程中创建一个本地NSPort实例B,也添加到NSRunLoop中。
- 从B向A发送消息后,将线程S的runloop运行。
- 主线程收到线程S发送的消息。
- 主线程向线程S发送消息。
- 通过
handlePortMessage:
代理方法接受消息。
三、NSPort实例
网上找了很久就看到一篇关于NSPort实例的文章,然后发现现实有错误。我使用的是Xcode 12.3和iOS14SDK。
在一个VC中实现
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"viewDidLoad thread is %@.", [NSThread currentThread]);
self.myPort = [NSMachPort port];
self.myPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
self.work = [[MyWorkerClass alloc] init];
[NSThread detachNewThreadSelector:@selector(launchThreadWithPort:) toTarget:self.work withObject:self.myPort];
}
- (void)handlePortMessage:(NSMessagePort *)message
{
NSLog(@"接受到子线程传递的消息。%@", message);
NSUInteger msgId = [[message valueForKeyPath:@"msgid"] integerValue];
NSMachPort *localPort = [message valueForKeyPath:@"localPort"];
NSMachPort *remotePort = [message valueForKeyPath:@"remotePort"];
NSMutableArray *componts = [message valueForKey:@"components"];
for (NSData *data in componts) {
NSLog(@"data is %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}
if (msgId == kMsg1) {
[remotePort sendBeforeDate:[NSDate date] msgid:kMsg2 components:nil from:localPort reserved:0];
}
}
在VC的头部引入自定义线程类
#import "MyWorkerClass.h"
#define kMsg1 100
#define kMsg2 101
自定义线程类MyWorkerClass的h文件暴露方法
- (void)launchThreadWithPort:(NSPort *)port;
m文件实现
#define kMsg1 100
#define kMsg2 101
@interface MyWorkerClass () <NSMachPortDelegate>
@property (nonatomic, strong) NSPort *remotePort;
@property (nonatomic, strong) NSPort *myPort;
@end
@implementation MyWorkerClass
- (void)launchThreadWithPort:(NSPort *)port
{
@autoreleasepool {
self.remotePort = port;
[[NSThread currentThread] setName:@"MyWorkerClass"];
NSLog(@"launchThreadWithPort thread is %@.", [NSThread currentThread]);
self.myPort = [NSMachPort port];
self.myPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
[self sendPortMessage];
[[NSRunLoop currentRunLoop] run];
}
}
- (void)sendPortMessage
{
NSData *data1 = [@"wang" dataUsingEncoding:NSUTF8StringEncoding];
NSData *data2 = [@"yinan" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array = [[NSMutableArray alloc] initWithArray:@[@"1", @"2"]];
[self.remotePort sendBeforeDate:[NSDate date] msgid:kMsg1 components:array from:self.myPort reserved:0];
}
- (void)handlePortMessage:(NSMessagePort *)message
{
NSLog(@"接受到父类的消息。。。%@。", message);
}
运行打印为
MyDream[11004:357602] viewDidLoad thread is <NSThread: 0x600003820100>{number = 1, name = main}.
MyDream[11004:357779] launchThreadWithPort thread is <NSThread: 0x60000384d440>{number = 8, name = MyWorkerClass}.
MyDream[11004:357602] 接受到子线程传递的消息。<NSPortMessage: 0x600003874d80>
MyDream[11004:357602] data is wang
MyDream[11004:357602] data is yinan
MyDream[11004:357779] 接受到父类的消息。。。<NSPortMessage: 0x600003838900>。
四、注意点
1. handlePortMessage方法
查看NSMachPort的delegate属性为NSMachPortDelegate,而NSPort的delegate属性为NSPortDelegate。其中NSMachPortDelegate继承NSPortDelegate。只要实现其中一个就可以。
@protocol NSPortDelegate <NSObject>
@optional
- (void)handlePortMessage:(NSPortMessage *)message;
// This is the delegate method that subclasses should send
// to their delegates, unless the subclass has something
// more specific that it wants to try to send first
@end
有一个问题是NSPortMessage是MacOS上使用了,所以在iOS上开发的时候就一直报错。最后修改为- (void)handlePortMessage:(NSMessagePort *)message
,使用NSMessagePort来接受消息。
2. 子线程运行RunLoop
调用[[NSRunLoop currentRunLoop] run];
的时机不同,就决定是否子线程能够通过NSPort接受到主线程的消息。也就是必须将[[NSRunLoop currentRunLoop] run];
一定要放到[self sendPortMessage];
之后。
3. components的类型
在- (BOOL)sendBeforeDate:(NSDate *)limitDate msgid:(NSUInteger)msgID components:(nullable NSMutableArray *)components from:(nullable NSPort *)receivePort reserved:(NSUInteger)headerSpaceReserved;
发送消息时进行传递参数components。
当components的内容为字符串时,报警告
2021-01-27 09:57:24.521514+0800 MyDream[10747:331651] *** D.O. message send ignoring unknown component type '__NSCFConstantString'
2021-01-27 09:57:24.522004+0800 MyDream[10747:331651] *** D.O. message send ignoring unknown component type '__NSCFConstantString'
而当components的内容为class时,报警告
2021-01-27 09:41:22.425767+0800 MyDream[10541:318874] *** D.O. message send ignoring unknown component type 'MyWorkerData'
2021-01-27 09:41:22.425931+0800 MyDream[10541:318874] *** D.O. message send ignoring unknown component type 'MyWorkerData'
一头雾水又度娘无解的时候,看到了
// The components array consists of a series of instances
// of some subclass of NSData, and instances of some
// subclass of NSPort; since one subclass of NSPort does
// not necessarily know how to transport an instance of
// another subclass of NSPort (or could do it even if it
// knew about the other subclass), all of the instances
// of NSPort in the components array and the 'receivePort'
// argument MUST be of the same subclass of NSPort that
// receives this message. If multiple DO transports are
// being used in the same program, this requires some care.
按苹果的注释习惯应该放在方法的上面的,这个竟然是在下方,导致一致被忽略了。尴尬尴尬。主要意思就是components需要是NSData组成。将内容修改为NSData后
传递参数成功。
五、performSelector实现线程间通信
NSThread中提供了
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
直接指定线程来执行方法,方便简洁。
写一个demo来show一把。将上面的修改一把,VC中的代码不用修改,MyWorkerClass的m文件修改如下:
#import "MyWorkerClass.h"
#define kMsg1 100
#define kMsg2 101
@interface MyWorkerClass () <NSMachPortDelegate>
@property (nonatomic, strong) NSPort *remotePort;
@property (nonatomic, strong) NSPort *myPort;
@property (nonatomic, strong) NSThread *thread;
@end
@implementation MyWorkerClass
- (void)launchThreadWithPort:(NSPort *)port
{
@autoreleasepool {
self.remotePort = port;
self.thread = [NSThread currentThread];
[[NSThread currentThread] setName:@"MyWorkerClass"];
NSLog(@"launchThreadWithPort thread is %@.", [NSThread currentThread]);
self.myPort = [NSMachPort port];
self.myPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
[self sendPortMessage];
[[NSRunLoop currentRunLoop] run];
}
}
- (void)sendPortMessage
{
[self performSelectorOnMainThread:@selector(runInMainThread:) withObject:@"往往" waitUntilDone:YES];
}
- (void)runInMainThread:(NSString *)name
{
NSLog(@"runInMainThread thread is %@.", [NSThread currentThread]);
NSLog(@"runInMainThread name is %@.", name);
// [self performSelector:@selector(runInThread:) onThread:self.thread withObject:@"牛牛汪汪" waitUntilDone:NO];
[self performSelector:@selector(runInThread:) onThread:self.thread withObject:@"牛牛汪汪" waitUntilDone:NO modes:@[NSRunLoopCommonModes]];
}
- (void)runInThread:(NSString *)name
{
NSLog(@"runInThread thread is %@.", [NSThread currentThread]);
NSLog(@"runInThread name is %@.", name);
}
@end
打印为:
MyDream[11613:411566] viewDidLoad thread is <NSThread: 0x600000864580>{number = 1, name = main}.
MyDream[11613:411624] launchThreadWithPort thread is <NSThread: 0x6000008123c0>{number = 7, name = MyWorkerClass}.
MyDream[11613:411566] runInMainThread thread is <NSThread: 0x600000864580>{number = 1, name = main}.
MyDream[11613:411566] runInMainThread name is 往往.
MyDream[11613:411624] runInThread thread is <NSThread: 0x6000008123c0>{number = 7, name = MyWorkerClass}.
MyDream[11613:411624] runInThread name is 牛牛汪汪.
实现子线程和主线程互相通信,前提是子线程需要保活。