CFNotificationCenter
CFNotificationCenter
是一種發(fā)通知的對象姑原,用法上類似與NSNotificationCenter
,用法上就是先注冊為某個通知的觀察者悬而,然后再發(fā)送通知呜舒,這樣通知中心的觀察者們就能收到通知,大致類似于這樣
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霸饲,這就很香了为朋,它的使用流程如下
進程A先創(chuàng)建一個本地消息端口(create local port)
進程B創(chuàng)建一個遠程端口(create remote port)并連接到它
然后進程B就可以給進程A發(fā)送消息了(send request)
進程A收到B的消息,并能及時回復一個消息回去
可能看我說的感覺沒什么大不了的厚脉,不過第4步能夠及時回一個消息過去就很牛逼了习寸,這意味著你可以把你的API寫成帶block的形式,意思就是說你在應用層發(fā)一個消息過去傻工,可以在這里等待對方的消息回來融涣,而不是兩邊互相獨立做監(jiān)聽,對于應用層來說是一種非常舒服的回調(diào)方式精钮。
整個工作流程大致如下:
我們來看一下具體用法
// 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);
data
和msgId
都可以自定義,其中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ù)后還可以返回一個對象回去税稼。但是也要注意兩個小坑點:
先create local port烦秩,再create remote port,否則直接create remote port會創(chuàng)建失敗
回傳的return data記得要手動copy或者create郎仆,不要直接強轉(zhuǎn)一個受到ARC內(nèi)存管理的
NSData*
對象過去只祠,否則會引起內(nèi)存崩潰