iOS/macOS上兩種IPC機制探討及避坑指南

CFNotificationCenter

CFNotificationCenter是一種發(fā)通知的對象姑原,用法上類似與NSNotificationCenter,用法上就是先注冊為某個通知的觀察者悬而,然后再發(fā)送通知呜舒,這樣通知中心的觀察者們就能收到通知,大致類似于這樣

image-20220805143037983.png

CFNotificationCenter有3種類型,但是一個程序只能最多擁有一種類型

  • 一個分布式的通知中心笨奠,通過CFNotificationCenterGetDistributedCenter獲取

  • 一個本地通知中心,通過CFNotificationCenterGetLocalCenter獲取

  • 一個Darwin通知中心,通過CFNotificationCenterGetDarwinNotifyCenter獲取

Darwin通知中心

相比較傳統(tǒng)上我們使用的NSNotificationCenter袭蝗,Darwin通知中心的限制還是蠻多的,最大的限制在于不能傳遞參數(shù)(userInfo)過去般婆,只能干巴巴的發(fā)個通知到腥,導致我第一次想用它來傳個參數(shù)的時候硬是傳不過去。它的注釋是這么說的

// 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.

不過盡管如此蔚袍,簡單的發(fā)個通知還是可以的乡范,使用方式如下:

// 對于主進程
// 1\. 先添加一個observer
 CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge  const void *)(self), onHostAppServerRequestCallback, (__bridge CFStringRef)[IPCConfig ipcHostAppServerRequestNotificationName], NULL, CFNotificationSuspensionBehaviorDeliverImmediately);

// 對于擴展進程
// 2\. 發(fā)送一個通知
 CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(),(__bridge CFStringRef)[IPCConfig ipcHostAppServerRequestNotificationName],NULL,nil,YES);

// 3\. 此時對于主進程來講,observer接到通知后啤咽,會啟動回調(diào)onHostAppServerRequestCallback
static void onHostAppServerRequestCallback(CFNotificationCenterRef center,
 void *observer, CFStringRef name,
 const void *object, CFDictionaryRef
 userInfo)
{
//應答晋辆,在這里是做實際的業(yè)務(wù)工作,其中observer你可能需要強轉(zhuǎn)成你之前注冊進去的類宇整,比如像我這樣
 IPCHostAppServer *server = (__bridge IPCHostAppServer*)observer;
 // 將一些參數(shù)比如block之類的綁在IPCHostAppServer,來進行上層的業(yè)務(wù)活動
}

// 4\. 移除觀察者
 CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge  const void *)(self), (__bridge CFStringRef)[IPCConfig ipcHostAppServerRequestNotificationName], NULL);</pre>

分發(fā)通知中心

CFNotificationCenterGetDistributedCenter有個很大限制就是只能在win32和OSX上用

#if TARGET_OS_OSX || TARGET_OS_WIN32
CF_EXPORT CFNotificationCenterRef CFNotificationCenterGetDistributedCenter(void);
#endif

另外兩種通知中心我這邊還沒有嘗試過瓶佳,就不詳細展開講了,下面介紹一下另外一種可以傳參的方式

CFMessagePort

CFMessagePort借助runloop提供了一個通信信道,它可以在本機上跨進程傳輸任意數(shù)據(jù)鳞青,數(shù)據(jù)類型是CFData霸饲,這就很香了为朋,它的使用流程如下

  1. 進程A先創(chuàng)建一個本地消息端口(create local port)

  2. 進程B創(chuàng)建一個遠程端口(create remote port)并連接到它

  3. 然后進程B就可以給進程A發(fā)送消息了(send request)

  4. 進程A收到B的消息,并能及時回復一個消息回去

可能看我說的感覺沒什么大不了的厚脉,不過第4步能夠及時回一個消息過去就很牛逼了习寸,這意味著你可以把你的API寫成帶block的形式,意思就是說你在應用層發(fā)一個消息過去傻工,可以在這里等待對方的消息回來融涣,而不是兩邊互相獨立做監(jiān)聽,對于應用層來說是一種非常舒服的回調(diào)方式精钮。

整個工作流程大致如下:

image-20220805152114927.png

我們來看一下具體用法

// 1. 創(chuàng)建本地端口
/// 運行IPC服務(wù)端
/// @param callback 接收到消息的回調(diào)
- (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在創(chuàng)建的時候要保持跟它一致
    CFMessagePortRef localPort = CFMessagePortCreateLocal(nil, (__bridge  CFStringRef)self.portName, recvMessageCallback, &context, &shouldFreeInfo);
    if (localPort == NULL) {
        return NO;
    }
    
    _localPort = localPort;
        // 創(chuàng)建一個runloop source,要監(jiān)聽消息威鹿,你需要創(chuàng)建一個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創(chuàng)建一個遠端端口連接轨香,這里記得portname要保持一致
    CFMessagePortRef remotePort = CFMessagePortCreateRemote(nil, (__bridge CFStringRef)self.servicePortName);
    if (remotePort == NULL) {
                // 不要忘記判斷一下
        return;
    }

解釋一下忽你,第2步這里有個大坑,就是它可能會返回一個NULL指針臂容,這種情況存在于你先創(chuàng)建遠端端口科雳,而不是先創(chuàng)建本地端口,意思就是說第一步?jīng)]做脓杉,直接上來做第二步是會失敗的,盡管在官方文檔上有說iOS7以后不可用糟秘,但是我自己寫的時候還是可以使用的
update:CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, kCFRunLoopCommonModes);這里推薦加在commonModes上,根據(jù)實際測試來看球散,如果加在defaultMode尿赚,當用戶的runloop處于UITrackingRunLoopMode狀態(tài)下,是無法接收流的蕉堰,比如當發(fā)送錄屏流到主進程時凌净,因為這是一個持續(xù)的過程,當用戶在應用內(nèi)滑動頁面的時候屋讶,無法接收數(shù)據(jù)冰寻,導致所有的滑動狀態(tài)都無法收到。

好的皿渗,現(xiàn)在我們來做第3步斩芭,發(fā)數(shù)據(jù)

3\. B發(fā)送數(shù)據(jù)給A
status = CFMessagePortSendRequest(remotePort, msgId, (__bridge CFDataRef)data, timeout, timeout, NULL, NULL);

datamsgId都可以自定義,其中msgId我理解是把它當做一個消息枚舉來使用乐疆,用于在進程A那邊來判斷這個發(fā)過來的數(shù)據(jù)類型划乖,最后timeout是超時時間,這個我感覺沒什么用诀拭,至少在我使用的過程中很少有遇到超時現(xiàn)象迁筛,不過也可能是讓應用層來判斷這不是保證交付的,所以要設(shè)置一些策略去規(guī)避不能傳達該怎么辦,我在這方面做的是在一定的時間和次數(shù)內(nèi)失敗是會不斷重復傳的细卧,最后兩個參數(shù)你不需要的話可以傳NULL尉桩,表示不需要等B的回傳數(shù)據(jù)。

此時贪庙,A的回調(diào)函數(shù)就會收到B發(fā)來的數(shù)據(jù)蜘犁,包含msgId和data

static CFDataRef recvMessageCallback(CFMessagePortRef port, SInt32 messageID, CFDataRef data, void *info) {
        // ... A中收到數(shù)據(jù)
};

最后如果不要port的時候記得釋放,蘋果對非ARC內(nèi)存對象的管理原則是名字中帶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
至此这橙,我們已經(jīng)完成了B對A發(fā)送數(shù)據(jù)。這時候我們還可以做的更好點兒嗎导披,比如說B發(fā)了數(shù)據(jù)給A屈扎,并能拿到一個A返回的數(shù)據(jù)回調(diào),而不是另外在寫一套監(jiān)聽方式去監(jiān)聽A發(fā)數(shù)據(jù)給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處拿到的返回數(shù)據(jù)止毕。而對應在A的回調(diào)函數(shù)對應的就是

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函數(shù)指針的返回類型就是A返回給B的數(shù)據(jù),現(xiàn)在咱們來修改一下我們的代碼

// A處的函數(shù)回調(diào)
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環(huán)境下的卤妒,可能是一個臨時變量,過了它所在代碼塊叠必,其生命周期就會結(jié)束荚孵,被releasepool給回收,導致壞內(nèi)存訪問纬朝,一會兒你就會崩在主線程的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;

現(xiàn)在我們回到第3步骄呼,調(diào)整一下代碼

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

總結(jié)

iOS和OSX上IPC有很多方式共苛,本文探討了其中的兩種,除此之外還有通過TCP/UDP進行常規(guī)通信的方式蜓萄。CFNotificationCenter類似于NSNotificationCenter隅茎,用法上類似于注冊觀察者,發(fā)送通知嫉沽,接收通知辟犀,移除觀察者,缺點是不能發(fā)送字段對象绸硕,有一定限制堂竟。

CFMessagePort相比較而言限制要小很多魂毁,可以發(fā)送任意數(shù)據(jù),先創(chuàng)建本地端口監(jiān)聽出嘹,再在另外一個進程進行連接席楚,最后再發(fā)送數(shù)據(jù),收到數(shù)據(jù)后還可以返回一個對象回去税稼。但是也要注意兩個小坑點:

  1. 先create local port烦秩,再create remote port,否則直接create remote port會創(chuàng)建失敗

  2. 回傳的return data記得要手動copy或者create郎仆,不要直接強轉(zhuǎn)一個受到ARC內(nèi)存管理的NSData*對象過去只祠,否則會引起內(nèi)存崩潰

推薦閱讀

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市扰肌,隨后出現(xiàn)的幾起案子铆农,更是在濱河造成了極大的恐慌,老刑警劉巖狡耻,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件墩剖,死亡現(xiàn)場離奇詭異,居然都是意外死亡夷狰,警方通過查閱死者的電腦和手機岭皂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沼头,“玉大人爷绘,你說我怎么就攤上這事〗叮” “怎么了活玲?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長岂座。 經(jīng)常有香客問我胶背,道長,這世上最難降的妖魔是什么垂蜗? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任楷扬,我火速辦了婚禮,結(jié)果婚禮上贴见,老公的妹妹穿的比我還像新娘烘苹。我一直安慰自己,他們只是感情好片部,可當我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布镣衡。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪廊鸥。 梳的紋絲不亂的頭發(fā)上望浩,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天,我揣著相機與錄音黍图,去河邊找鬼曾雕。 笑死,一個胖子當著我的面吹牛助被,可吹牛的內(nèi)容都是我干的剖张。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼揩环,長吁一口氣:“原來是場噩夢啊……” “哼搔弄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起丰滑,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤顾犹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后褒墨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體炫刷,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年郁妈,在試婚紗的時候發(fā)現(xiàn)自己被綠了浑玛。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡噩咪,死狀恐怖顾彰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胃碾,我是刑警寧澤涨享,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站仆百,受9級特大地震影響厕隧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜儒旬,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一栏账、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧栈源,春花似錦、人聲如沸竖般。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至艰亮,卻和暖如春闭翩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背迄埃。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工疗韵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人侄非。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓蕉汪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親逞怨。 傳聞我的和親對象是個殘疾皇子者疤,可洞房花燭夜當晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容