CFNotificationCenter
CFNotificationCenter
是一种发通知的对象,用法上类似与NSNotificationCenter
,用法上就是先注册为某个通知的观察者,然后再发送通知,这样通知中心的观察者们就能收到通知,大致类似于这样
CFNotificationCenter
有3种类型,但是一个程序只能最多拥有一种类型
一个分布式的通知中心,通过
CFNotificationCenterGetDistributedCenter
获取一个本地通知中心,通过
CFNotificationCenterGetLocalCenter
获取一个Darwin通知中心,通过
CFNotificationCenterGetDarwinNotifyCenter
获取
Darwin通知中心
相比较传统上我们使用的NSNotificationCenter
,Darwin通知中心的限制还是蛮多的,最大的限制在于不能传递参数(userInfo)过去,只能干巴巴的发个通知,导致我第一次想用它来传个参数的时候硬是传不过去。它的注释是这么说的
// The Darwin Notify Center is based on the <notify.h> API.
// For this center, there are limitations in the API. There are no notification "objects",
// "userInfo" cannot be passed in the notification, and there are no suspension behaviors
// (always "deliver immediately"). Other limitations in the <notify.h> API as described in
// that header will also apply.
// - In the CFNotificationCallback, the 'object' and 'userInfo' parameters must be ignored.
// - CFNotificationCenterAddObserver(): the 'object' and 'suspensionBehavior' arguments are ignored.
// - CFNotificationCenterAddObserver(): the 'name' argument may not be NULL (for this center).
// - CFNotificationCenterRemoveObserver(): the 'object' argument is ignored.
// - CFNotificationCenterPostNotification(): the 'object', 'userInfo', and 'deliverImmediately' arguments are ignored.
// - CFNotificationCenterPostNotificationWithOptions(): the 'object', 'userInfo', and 'options' arguments are ignored.
// The Darwin Notify Center has no notion of per-user sessions, all notifications are system-wide.
// As with distributed notifications, the main thread's run loop must be running in one of the
// common modes (usually kCFRunLoopDefaultMode) for Darwin-style notifications to be delivered.
// NOTE: NULL or 0 should be passed for all ignored arguments to ensure future compatibility.
不过尽管如此,简单的发个通知还是可以的,使用方式如下:
// 对于主进程
// 1\. 先添加一个observer
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), onHostAppServerRequestCallback, (__bridge CFStringRef)[IPCConfig ipcHostAppServerRequestNotificationName], NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
// 对于扩展进程
// 2\. 发送一个通知
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(),(__bridge CFStringRef)[IPCConfig ipcHostAppServerRequestNotificationName],NULL,nil,YES);
// 3\. 此时对于主进程来讲,observer接到通知后,会启动回调onHostAppServerRequestCallback
static void onHostAppServerRequestCallback(CFNotificationCenterRef center,
void *observer, CFStringRef name,
const void *object, CFDictionaryRef
userInfo)
{
//应答,在这里是做实际的业务工作,其中observer你可能需要强转成你之前注册进去的类,比如像我这样
IPCHostAppServer *server = (__bridge IPCHostAppServer*)observer;
// 将一些参数比如block之类的绑在IPCHostAppServer,来进行上层的业务活动
}
// 4\. 移除观察者
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge const void *)(self), (__bridge CFStringRef)[IPCConfig ipcHostAppServerRequestNotificationName], NULL);</pre>
分发通知中心
CFNotificationCenterGetDistributedCenter
有个很大限制就是只能在win32和OSX上用
#if TARGET_OS_OSX || TARGET_OS_WIN32
CF_EXPORT CFNotificationCenterRef CFNotificationCenterGetDistributedCenter(void);
#endif
另外两种通知中心我这边还没有尝试过,就不详细展开讲了,下面介绍一下另外一种可以传参的方式
CFMessagePort
CFMessagePort借助runloop提供了一个通信信道,它可以在本机上跨进程传输任意数据,数据类型是CFData,这就很香了,它的使用流程如下
进程A先创建一个本地消息端口(create local port)
进程B创建一个远程端口(create remote port)并连接到它
然后进程B就可以给进程A发送消息了(send request)
进程A收到B的消息,并能及时回复一个消息回去
可能看我说的感觉没什么大不了的,不过第4步能够及时回一个消息过去就很牛逼了,这意味着你可以把你的API写成带block的形式,意思就是说你在应用层发一个消息过去,可以在这里等待对方的消息回来,而不是两边互相独立做监听,对于应用层来说是一种非常舒服的回调方式。
整个工作流程大致如下:
我们来看一下具体用法
// 1. 创建本地端口
/// 运行IPC服务端
/// @param callback 接收到消息的回调
- (BOOL)runWithCallback:(__nullable CFDataRef(^)(SInt32, NSData *))callback {
if (!(self.portName.length > 0)) {
return NO;
}
DDLogDebug(@"runWithCallback from local, portName = %@",self.portName);
CFMessagePortContext context = {0, (__bridge void *)self, NULL, NULL};
Boolean shouldFreeInfo = false;
// 注意:这里的portName一会儿进程B在创建的时候要保持跟它一致
CFMessagePortRef localPort = CFMessagePortCreateLocal(nil, (__bridge CFStringRef)self.portName, recvMessageCallback, &context, &shouldFreeInfo);
if (localPort == NULL) {
return NO;
}
_localPort = localPort;
// 创建一个runloop source,要监听消息,你需要创建一个runloop source,一会儿再把它加到runloop里面
CFRunLoopSourceRef runLoopSource = CFMessagePortCreateRunLoopSource(nil, localPort, 0);
if (runLoopSource == NULL) {
return NO;
}
_ipcSource = runLoopSource;
self.callback = callback;
// 这里我加在主的RunLoop上,当然你也可以加在自己的runloop上
CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, kCFRunLoopCommonModes);
return YES;
}
// 2. 进程B创建一个远端端口连接,这里记得portname要保持一致
CFMessagePortRef remotePort = CFMessagePortCreateRemote(nil, (__bridge CFStringRef)self.servicePortName);
if (remotePort == NULL) {
// 不要忘记判断一下
return;
}
解释一下,第2步这里有个大坑,就是它可能会返回一个NULL指针,这种情况存在于你先创建远端端口,而不是先创建本地端口,意思就是说第一步没做,直接上来做第二步是会失败的,尽管在官方文档上有说iOS7以后不可用,但是我自己写的时候还是可以使用的
update:CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, kCFRunLoopCommonModes);
这里推荐加在commonModes上,根据实际测试来看,如果加在defaultMode,当用户的runloop处于UITrackingRunLoopMode
状态下,是无法接收流的,比如当发送录屏流到主进程时,因为这是一个持续的过程,当用户在应用内滑动页面的时候,无法接收数据,导致所有的滑动状态都无法收到。
好的,现在我们来做第3步,发数据
3\. B发送数据给A
status = CFMessagePortSendRequest(remotePort, msgId, (__bridge CFDataRef)data, timeout, timeout, NULL, NULL);
data
和msgId
都可以自定义,其中msgId
我理解是把它当做一个消息枚举来使用,用于在进程A那边来判断这个发过来的数据类型,最后timeout
是超时时间,这个我感觉没什么用,至少在我使用的过程中很少有遇到超时现象,不过也可能是让应用层来判断这不是保证交付的,所以要设置一些策略去规避不能传达该怎么办,我在这方面做的是在一定的时间和次数内失败是会不断重复传的,最后两个参数你不需要的话可以传NULL,表示不需要等B的回传数据。
此时,A的回调函数就会收到B发来的数据,包含msgId和data
static CFDataRef recvMessageCallback(CFMessagePortRef port, SInt32 messageID, CFDataRef data, void *info) {
// ... A中收到数据
};
最后如果不要port的时候记得释放,苹果对非ARC内存对象的管理原则是名字中带create和copy的都需要我们手动管理
// 进程B释放remote port
CFRelease(remotePort);
// 进程A移除runloop source,并释放localport
if (_ipcSource != NULL) {
CFRunLoopRemoveSource(CFRunLoopGetMain(), _ipcSource, kCFRunLoopCommonModes);
CFRelease(_ipcSource);
}
if (_localPort != NULL) {
CFMessagePortInvalidate(_localPort);
CFRelease(_localPort);
}
update:同理,这里也要切换为kCFRunLoopCommonModes
至此,我们已经完成了B对A发送数据。这时候我们还可以做的更好点儿吗,比如说B发了数据给A,并能拿到一个A返回的数据回调,而不是另外在写一套监听方式去监听A发数据给B,使得代码不够聚合。当然可以!
/* NULL replyMode argument means no return value expected, don't wait for it */
CF_EXPORT SInt32 CFMessagePortSendRequest(CFMessagePortRef remote, SInt32 msgid, CFDataRef data, CFTimeInterval sendTimeout, CFTimeInterval rcvTimeout, CFStringRef replyMode, CFDataRef *returnData);
其中returnData就是我们从A处拿到的返回数据。而对应在A的回调函数对应的就是
typedef CFDataRef (*CFMessagePortCallBack)(CFMessagePortRef local, SInt32 msgid, CFDataRef data, void *info);
CF_EXPORT CFMessagePortRef CFMessagePortCreateLocal(CFAllocatorRef allocator, CFStringRef name, CFMessagePortCallBack callout, CFMessagePortContext *context, Boolean *shouldFreeInfo);
其中CFMessagePortCallBack函数指针的返回类型就是A返回给B的数据,现在咱们来修改一下我们的代码
// A处的函数回调
static CFDataRef recvMessageCallback(CFMessagePortRef port, SInt32 messageID, CFDataRef data, void *info) {
// data messageId job
// get CFDataRef from info
return data;
};
这里有最后一个大坑,注意这里的data不要从应用层直接桥接!比如说应用层传过来一个NSData *
类型,可能你直接(__bridge CFDataRef)data
丢进去,那这样就大错特错了,不要忘了,我们的NSData是在ARC环境下的,可能是一个临时变量,过了它所在代码块,其生命周期就会结束,被releasepool给回收,导致坏内存访问,一会儿你就会崩在主线程的runloop上一脸懵逼。这时候我们要自己create一个CFData传过去
NSData *data; // your data from application
const UInt8* bytes = data.bytes;
CFIndex length = data.length;
CFDataRef dataRef = CFDataCreate(NULL, bytes, length);
return dataRef;
现在我们回到第3步,调整一下代码
CFDataRef recvData;
status = CFMessagePortSendRequest(remotePort, msgId, (__bridge CFDataRef)data, timeout, timeout, kCFRunLoopDefaultMode, &recvData);
if (status == kCFMessagePortSuccess) {
const UInt8* pData = CFDataGetBytePtr(recvData);
long datalen = CFDataGetLength(recvData);
// 拿到B回传的data
NSData *oc_data = [[NSData alloc] initWithBytes:pData length:datalen];
CFRelease(recvData);
}
总结
iOS和OSX上IPC有很多方式,本文探讨了其中的两种,除此之外还有通过TCP/UDP进行常规通信的方式。CFNotificationCenter
类似于NSNotificationCenter
,用法上类似于注册观察者,发送通知,接收通知,移除观察者,缺点是不能发送字段对象,有一定限制。
CFMessagePort
相比较而言限制要小很多,可以发送任意数据,先创建本地端口监听,再在另外一个进程进行连接,最后再发送数据,收到数据后还可以返回一个对象回去。但是也要注意两个小坑点:
先create local port,再create remote port,否则直接create remote port会创建失败
回传的return data记得要手动copy或者create,不要直接强转一个受到ARC内存管理的
NSData*
对象过去,否则会引起内存崩溃