iOS/macOS上两种IPC机制探讨及避坑指南

CFNotificationCenter

CFNotificationCenter是一种发通知的对象,用法上类似与NSNotificationCenter,用法上就是先注册为某个通知的观察者,然后再发送通知,这样通知中心的观察者们就能收到通知,大致类似于这样

image-20220805143037983.png

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,这就很香了,它的使用流程如下

  1. 进程A先创建一个本地消息端口(create local port)

  2. 进程B创建一个远程端口(create remote port)并连接到它

  3. 然后进程B就可以给进程A发送消息了(send request)

  4. 进程A收到B的消息,并能及时回复一个消息回去

可能看我说的感觉没什么大不了的,不过第4步能够及时回一个消息过去就很牛逼了,这意味着你可以把你的API写成带block的形式,意思就是说你在应用层发一个消息过去,可以在这里等待对方的消息回来,而不是两边互相独立做监听,对于应用层来说是一种非常舒服的回调方式。

整个工作流程大致如下:

image-20220805152114927.png

我们来看一下具体用法

// 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);

datamsgId都可以自定义,其中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相比较而言限制要小很多,可以发送任意数据,先创建本地端口监听,再在另外一个进程进行连接,最后再发送数据,收到数据后还可以返回一个对象回去。但是也要注意两个小坑点:

  1. 先create local port,再create remote port,否则直接create remote port会创建失败

  2. 回传的return data记得要手动copy或者create,不要直接强转一个受到ARC内存管理的NSData*对象过去,否则会引起内存崩溃

推荐阅读

CFMessagePort官方文档
CFNotificationCenter官方文档
深入理解RunLoop

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容