iOS系統是出了名的封閉,每個應用的活動范圍被嚴格地限制在各自的沙盒中。盡管如此,iOS還是提供了若干進程間通信機制,CFMessagePort就是其中之一。
從類名可以看出,CFMessagePort屬於Core Foundation層的東西,其實現部分是開源的,代碼在可以在蘋果的開源代碼庫中找到。
使用方式
1、消息接收者
CFMessagePort端口消息的接收者需要實現以下功能:
1.1 注冊監聽
消息接收者需要通過以下方式注冊消息監聽:
-(void)startListenning { if (0 != mMsgPortListenner && CFMessagePortIsValid(mMsgPortListenner)) { CFMessagePortInvalidate(mMsgPortListenner); } mMsgPortListenner = CFMessagePortCreateLocal(kCFAllocatorDefault,CFSTR(LOCAL_MACH_PORT_NAME),onRecvMessageCallBack, NULL, NULL); CFRunLoopSourceRef source = CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, mMsgPortListenner, 0); CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes); NSLog(@"start listenning"); }
其中LOCAL_MACH_PORT_NAME的定義為:
#define LOCAL_MACH_PORT_NAME "com.wangzz.demo"
經過查看源碼發現,CFMessagePort實際上是通過mach port實現的。Mach port是iOS系統提供的基於端口的輸入源,可用於線程或進程間通訊。而Runloop支持的輸入源類型中就包括基於端口的輸入源,因此可以使用Runloop做為CFMessagePort端口源事件的監聽者。
上述代碼有幾點需要說明:
通過CFMessagePortCreateLocal可以創建一個本地CFMessagePortRef對象
CFMessagePort對象是靠一個字符串來唯一標識的,這一點非常重要,在這裡字符串是由宏LOCAL_MACH_PORT_NAME定義的;
創建CFMessagePort對象的同時設置了端口源事件的回調函數onRecvMessageCallBack,用於處理端口源事件;
將創建的對象作為輸入源添加到Runloop中,從而實現對端口源事件的監聽,當Runloop收到對應的端口源事件時,會調用上一步中指定的回調方法;
1.2 實現回調方法
回調函數為CFMessagePortCallBack類型,其定義部分為:
typedef CFDataRef (*CFMessagePortCallBack) ( CFMessagePortRef local, SInt32 msgid, CFDataRef data, void *info );
各個參數的含義為:
CFMessagePortRef local
當前接收消息的CFMessagePortRef對象。
SInt32 msgid
這個字段非常有用,用於標識消息。如果通信雙方進程約定號每個msgid對應的數據結構,即可實現較為復雜的通信。
CFDataRef data
通信的真正數據部分。
void *info
為使用CFMessagePortCreateLocal方法創建port端口時指定的CFMessagePortContext對象的info字段,通常為空。
該回調方法可以返回一個CFDataRef類型的數據給port消息的發送者,以實現有效的雙方通信,這一點也非常重要。
我的回調函數onRecvMessageCallBack的實現:
CFDataRef onRecvMessageCallBack(CFMessagePortRef local,SInt32 msgid,CFDataRef cfData, void*info) { NSLog(@"onRecvMessageCallBack is called"); NSString *strData = nil; if (cfData) { const UInt8 * recvedMsg = CFDataGetBytePtr(cfData); strData = [NSString stringWithCString:(char *)recvedMsg encoding:NSUTF8StringEncoding]; /** 實現數據解析操作 **/ NSLog(@"receive message:%@",strData); } //為了測試,生成返回數據 NSString *returnString = [NSString stringWithFormat:@"i have receive:%@",strData]; const char* cStr = [returnString UTF8String]; NSUInteger ulen = [returnString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; CFDataRef sgReturn = CFDataCreate(NULL, (UInt8 *)cStr, ulen); return sgReturn; }
該方法實現的較為簡單,解析約定的數據(測試代碼中約定傳送的是string),為了測試,同時生成一個CFDataRef數據返回給port消息的發送者。
1.3 取消端口監聽
可以通過如下方式取消對port端口的監聽:
- (void)endLisenning { CFMessagePortInvalidate(mMsgPortListenner); CFRelease(mMsgPortListenner); }
CFMessagePortInvalidate會停止port消息的發送和接收操作,而只有調用了CFRelease,CFMessagePortRef對象才真正的被釋放掉。
2、消息發送者
發送部分代碼如下:
-(NSString *)sendMessageToDameonWith:(id)msgInfo msgID:(NSInteger)msgid { // 生成Remote port CFMessagePortRef bRemote = CFMessagePortCreateRemote(kCFAllocatorDefault, CFSTR(MACH_PORT_REMOTE)); if (nil == bRemote) { NSLog(@"bRemote create failed"); return nil; } // 構建發送數據(string) NSString *msg = [NSString stringWithFormat:@"%@",msgInfo]; NSLog(@"send msg is :%@",msg); const char *message = [msg UTF8String]; CFDataRef data,recvData = nil; data = CFDataCreate(NULL, (UInt8 *)message, strlen(message)); // 執行發送操作 CFMessagePortSendRequest(bRemote, msgid, data, 0, 100 , kCFRunLoopDefaultMode, &recvData); if (nil == recvData) { NSLog(@"recvData date is nil."); CFRelease(data); CFMessagePortInvalidate(bRemote); CFRelease(bRemote); return nil; } // 解析返回數據 const UInt8 * recvedMsg = CFDataGetBytePtr(recvData); if (nil == recvedMsg) { NSLog(@"receive date err."); CFRelease(data); CFMessagePortInvalidate(bRemote); CFRelease(bRemote); return nil; } NSString *strMsg = [NSString stringWithCString:(char *)recvedMsg encoding:NSUTF8StringEncoding]; NSLog(@"%@",strMsg); CFRelease(data); CFMessagePortInvalidate(bRemote); CFRelease(bRemote); CFRelease(recvData); return strMsg; }
其中MACH_PORT_REMOTE的定義為:
#define MACH_PORT_REMOTE "com.wangzz.demo"
發送消息時要相對簡單,首先通過CFMessagePortCreateRemote生成一個Remote的CFMessagePortRef,這裡需要注意的是CFMessagePortCreateRemote時傳入的字符串唯一標識MACH_PORT_REMOTE必須和消息接收者創建local的CFMessagePortRef時使用的字符串唯一標識是同一個!
通過查看源碼發現,CFMessagePortCreateRemote會根據MACH_PORT_REMOTE定義的字符串為唯一標識獲取消息接收者通過CFMessagePortCreateLocal使用相同字符串創建的底層mach port端口,從而實現向消息接收者發送信息。
如果消息接收者還沒有創建或者通過CFMessagePortCreateLocal創建local端口失敗時,想要通過CFMessagePortCreateRemote去創建remote端口肯定是失敗的。
說明
很遺憾的是,在iOS7及以後系統中,CFMessagePort的通信機制不再可用。
在使用CFMessagePortCreateLocal/CFMessagePortCreateRemote創建CFMessagePortRef對象時會失敗,官方文檔中是這麼說的:
This method is not available on iOS 7 and later—it will return NULL and log a sandbox violation in syslog. See Concurrency Programming Guide for possible replacement technologies.
CFMessagePort只能用於本地進程通信。
CFMessagePort是基於mach port端口的通信方式,不但可以用於進程通信,也可以用於線程間通信,只是線程間通信有了GCD和Cocoa提供的原生方法,已經能很方便的實現了,沒必要再使用CFMessagePort。
進程通信使用場景
iOS系統多任務機制,使得進程間通信基本都只能用於越獄開發。常用的場景是前端有一個UI程序用於界面展示,後端有一個daemo精靈程序用於任務處理。
demo工程
特地做了了個demo工程,以便更好地演示CFMessagePort的使用,可以到CSDN下載。
為了模擬進程間通信場景,我將消息接收進程CFMessagePortReceive做成了能夠後台播放音樂的程序,以便其切到後台後能繼續存活。
由於CFMessagePort不再支持iOS7及以後系統,本demo實在iOS6系統上測試的。
demo使用方式:
CFMessagePortReceive啟動後,點擊Start Listenning創建CFMessagePort接口並開始監聽port消息,然後將CFMessagePortReceive切到後台;
啟動CFMessagePortSend程序,在輸入框中寫入內容,點擊發送按鈕即可和CFMessagePortReceive通信。
MessagePort通信過程中會有日志輸出,可以使用以下方式查看日志:
1.真機
選擇:Xcode->Window->Organizer->Devices,然後選中窗口左側當前設備的Console窗口查看。
2.模擬器
選擇:模擬器->調試->打開系統日志,或者直接使用快捷鍵?/直接打開系統控制台查看日志。
參考文檔
CF-855.14
Threading Programming Guide
CFMessagePort Reference