總起
OS X是MacOS與NeXTSTEP的結(jié)合。OC是Smalltalk類面向?qū)ο缶幊膛cC的結(jié)合纵诞。iCloud則是蘋果移動(dòng)服務(wù)與云平臺(tái)的結(jié)合。
上述都是一些亮點(diǎn),但是不得不說蘋果技術(shù)中的進(jìn)程通訊走的是“反人類”的道路室梅。
由于不是根據(jù)每個(gè)節(jié)點(diǎn)上最優(yōu)原則進(jìn)行設(shè)計(jì),蘋果的進(jìn)程間通信解決方案更顯得混亂扎堆疚宇。結(jié)果是竞惋,大量重疊,不兼容的IPC技術(shù)在各個(gè)抽象層隨處可見灰嫉。(除了GCD還有剪貼板)
Mach Ports
Distributed Notifications
Distributed Objects
AppleEvents & AppleScript
Pasteboard
XPC
從低級(jí)內(nèi)核抽象到高級(jí)拆宛,面向?qū)ο蟮腁PI,它們都有各自特殊的表現(xiàn)以及安全特性讼撒。但是基礎(chǔ)層面來看浑厚,它們都是從不同上下文段傳遞或者獲取數(shù)據(jù)的機(jī)制。
分述
Mach Ports
所有的進(jìn)程間通訊最終落實(shí)依賴的還是Mach內(nèi)核API提供的功能根盒。
Mach端口是輕量并且強(qiáng)大的而又缺少相關(guān)文檔晦澀使用的(天使與惡魔)钳幅。
通過一個(gè)Mach端口發(fā)送一個(gè)消息調(diào)用一次mach_msg_send方法,但是這里需要做一些配置來構(gòu)建待發(fā)送的消息:
natural_t data;mach_port_t port;struct {? ? mach_msg_header_t header;? ? mach_msg_body_t body;? ? mach_msg_type_descriptor_ttype;} message;message.header = (mach_msg_header_t) {? ? .msgh_remote_port = port,? ? .msgh_local_port = MACH_PORT_NULL,? ? .msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND,0),? ? .msgh_size = sizeof(message)};message.body = (mach_msg_body_t) {? ? .msgh_descriptor_count =1};message.type = (mach_msg_type_descriptor_t) {? ? .pad1 = data,? ? .pad2 = sizeof(data)};mach_msg_return_t error = mach_msg_send(&message.header);if(error == MACH_MSG_SUCCESS) {? ? // ...}
(消息)接收端稍微輕松點(diǎn)炎滞,因?yàn)橄⒅恍枰宦暶鞫挥贸跏蓟?/p>
mach_port_t port;struct {? ? mach_msg_header_t header;? ? mach_msg_body_t body;? ? mach_msg_type_descriptor_ttype;? ? mach_msg_trailer_t trailer;} message;mach_msg_return_t error = mach_msg_receive(&message.header);if(error == MACH_MSG_SUCCESS) {? ? natural_t data = message.type.pad1;? ? // ...}
還算不錯(cuò)的是敢艰,Core Foundation和Foundation為Mach端口提供了高級(jí)API。在內(nèi)核基礎(chǔ)上封裝的CFMachPort / NSMachPort可以用做runloop源册赛,盡管CFMachPort / NSMachPort有利于的是兩個(gè)不同端口之間的通訊同步钠导。
CFMessagePort確實(shí)非常適合用于簡單的一對(duì)一通訊震嫉。簡簡單單幾行代碼,一個(gè)本地端口就被附屬到runloop源上牡属,只要獲取到消息就執(zhí)行回調(diào)票堵。
staticCFDataRef Callback(CFMessagePortRef port,? ? ? ? ? ? ? ? ? ? ? ? ? SInt32 messageID,? ? ? ? ? ? ? ? ? ? ? ? ? CFDataRef data,void*info){// ...}CFMessagePortRef localPort =? ? CFMessagePortCreateLocal(nil,? ? ? ? ? ? ? ? ? ? ? ? ? ? CFSTR("com.example.app.port.server"),? ? ? ? ? ? ? ? ? ? ? ? ? ? Callback,nil,nil);CFRunLoopSourceRef runLoopSource =? ? CFMessagePortCreateRunLoopSource(nil, localPort,0);CFRunLoopAddSource(CFRunLoopGetCurrent(),? ? ? ? ? ? ? ? ? runLoopSource,? ? ? ? ? ? ? ? ? kCFRunLoopCommonModes);
若要進(jìn)行發(fā)送數(shù)據(jù)同樣也十分直截了當(dāng)。只要完成指定遠(yuǎn)端的端口逮栅,裝載數(shù)據(jù)悴势,還有設(shè)置發(fā)送與接收的超時(shí)時(shí)間的操作。剩下就由CFMessagePortSendRequest來接管了措伐。
CFDataRef data;SInt32 messageID =0x1111;// ArbitraryCFTimeInterval timeout =10.0;CFMessagePortRef remotePort =? ? CFMessagePortCreateRemote(nil,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? CFSTR("com.example.app.port.client"));SInt32 status =? ? CFMessagePortSendRequest(remotePort,? ? ? ? ? ? ? ? ? ? ? ? ? ? messageID,? ? ? ? ? ? ? ? ? ? ? ? ? ? data,? ? ? ? ? ? ? ? ? ? ? ? ? ? timeout,? ? ? ? ? ? ? ? ? ? ? ? ? ? timeout,NULL,NULL);if(status == kCFMessagePortSuccess) {// ...}
Distributed Notifications
在Cocoa中有很多種兩個(gè)對(duì)象進(jìn)行通信的途徑特纤。
當(dāng)然也能進(jìn)行直接消息傳遞。也有像目標(biāo)-動(dòng)作侥加,代理捧存,回調(diào)這些解耦,一對(duì)一的設(shè)計(jì)模式官硝。KVO允許讓很多對(duì)象訂閱一個(gè)事件矗蕊,但是它把這些對(duì)象都聯(lián)系起來了。另一方面通知讓消息全局廣播氢架,并且讓有監(jiān)聽該廣播的對(duì)象接收該消息傻咖。【注:想知道發(fā)了多少次廣播嗎岖研?添加NSNotificationCenter addObserverForName:object:queue:usingBlock卿操,其中name與object置nil,看block被調(diào)用了幾次孙援『τ伲】
每個(gè)應(yīng)用為基礎(chǔ)應(yīng)用消息發(fā)布-訂閱對(duì)自身通知中心實(shí)例進(jìn)行管理。但是鮮有人知的APICFNotificationCenterGetDistributedCenter的通知可以進(jìn)行系統(tǒng)級(jí)別范圍的通信拓售。
為了獲取通知窥摄,添加所要指定監(jiān)聽消息名的觀察者到通知發(fā)布中心,當(dāng)消息接收到的時(shí)候函數(shù)指針指向的函數(shù)將被執(zhí)行一次:
staticvoidCallback(CFNotificationCenterRef center,void*observer,? ? ? ? ? ? ? ? ? ? CFStringRef name,constvoid*object,? ? ? ? ? ? ? ? ? ? CFDictionaryRef userInfo){// ...}CFNotificationCenterRef distributedCenter =? ? CFNotificationCenterGetDistributedCenter();CFNotificationSuspensionBehavior behavior =? ? ? ? CFNotificationSuspensionBehaviorDeliverImmediately;CFNotificationCenterAddObserver(distributedCenter,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? NULL,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Callback,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? CFSTR("notification.identifier"),? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? NULL,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? behavior);
發(fā)送端代碼更為簡單础淤,只要配置好ID,對(duì)象還有user info:
void*object;CFDictionaryRef userInfo;CFNotificationCenterRef distributedCenter =? ? CFNotificationCenterGetDistributedCenter();CFNotificationCenterPostNotification(distributedCenter,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? CFSTR("notification.identifier"),object,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? userInfo,true);
鏈接兩個(gè)應(yīng)用通信的方式中崭放,分發(fā)式通知是最為簡單的。用它來進(jìn)行大量數(shù)據(jù)的傳輸是不明智的鸽凶,但是對(duì)于輕量級(jí)信息同步币砂,分發(fā)式通知堪稱完美。
Distributed Objects
90年代中NeXT全盛時(shí)期玻侥,分發(fā)式對(duì)象(DO)是Cocoa框架中一個(gè)遠(yuǎn)程消息發(fā)送特性决摧。盡管現(xiàn)在已經(jīng)不再大范圍的使用,在現(xiàn)代奇數(shù)層上IPC無障礙通信仍然并未實(shí)現(xiàn)。
使用DO分發(fā)一個(gè)對(duì)象僅僅是搭建一個(gè)NSConnection并將其注冊(cè)為特殊(你分的清楚)的名字:
@protocolProtocol;id vendedObject;NSConnection *connection = [[NSConnection alloc] init];[connection setRootObject:vendedObject];[connection registerName:@"server"];
另外一個(gè)應(yīng)用將會(huì)也建立同樣名字的并注冊(cè)過的鏈接掌桩,然后立即獲取一個(gè)原子代理當(dāng)做原始對(duì)象边锁。
idproxy = [NSConnection rootProxyForConnectionWithRegisteredName:@"server"host:nil];[proxy setProtocolForProxy:@protocol(Protocol)];
只要分發(fā)對(duì)象代理收到消息了,一個(gè)通過NSConnection連接遠(yuǎn)程調(diào)用(RPC)將會(huì)根據(jù)發(fā)送對(duì)象進(jìn)行對(duì)應(yīng)的計(jì)算并且返回結(jié)果給代理拘鞋⊙馀睿【注:原理是一個(gè)OS管理的共享的NSPortNameServer實(shí)例對(duì)這個(gè)帶著名字的連接進(jìn)行管控矢门∨枭】
分發(fā)式對(duì)象簡單,透明祟剔,健壯隔躲。簡直就是Cocoa中的標(biāo)桿。物延。宣旱。
實(shí)際上,分布式對(duì)象不能像局部對(duì)象那樣使用叛薯,那就是因?yàn)槿魏伟l(fā)送給代理的消息都可能拋出異常浑吟。不想其他語言,OC沒有異常處理控制流程耗溜。所以對(duì)任何東西都進(jìn)行@try/@catch也算是Cocoa大會(huì)很凄涼的補(bǔ)救了组力。
DO還有一個(gè)原因致其使用不便。在試圖通過連接“marshal values”時(shí)抖拴,對(duì)象和原語的差距尤為明顯燎字。
此外,連接是完全加密的阿宅,和下方通信信道擴(kuò)展性的缺乏致使其在大多數(shù)的使用中通信被迫中斷候衍。
下方是左列分布式對(duì)象用來指定其屬性代理行為和方法參數(shù)的注解:
in:輸入?yún)?shù),后續(xù)不再引用
out:參數(shù)被引用作為返回值
inout:輸入?yún)?shù)洒放,引用作為返回值
const:常量參數(shù)
oneway:無障礙結(jié)果返回
bycopy:返回對(duì)象的拷貝
byref:返回對(duì)象的代理
AppleEvents & AppleScript
AppleEvents是經(jīng)典Macintosh操作系統(tǒng)最持久的遺產(chǎn)蛉鹿。在System 7推出的AppleEvents允許應(yīng)用程序在本地使用AppleScript或者使用程序鏈接的功能進(jìn)行程序控制。現(xiàn)在AppleScript使用Cocoa Scripting Bridge往湿,仍然是OS X應(yīng)用進(jìn)程間最直接的交互方式妖异。【注:Mac系統(tǒng)的蘋果時(shí)間管理中心為AppleEvents提供了原始低級(jí)傳送機(jī)制煌茴,但是是在OS X的Mach端口基礎(chǔ)之上的重實(shí)現(xiàn)】随闺。
也就是說,使用起來這是簡單而又古怪的技術(shù)之一蔓腐。
AppleScript使用自然語言語法矩乐,設(shè)計(jì)初衷是沒有涉及參數(shù)而更容易掌握。雖然與人交流更親和了,但是寫起來確實(shí)噩夢(mèng)散罕。
為了更好的了解人類自然性分歇,這里有個(gè)栗子教你怎么讓Safari在最前的窗口的激活欄打開一個(gè)URL。
tell application "Safari"setthe URLofthe front documentto"http://nshipster.com"endtell
在大部分情況下欧漱,AppleScript的語法自然語言的特性更多是不便不是優(yōu)勢(shì)职抡。(吐槽。误甚。缚甩。略略略)
即便是經(jīng)驗(yàn)老道的OC開發(fā)者,不靠文檔或者栗子寫出AppleScript是不可能的任務(wù)窑邦。
幸運(yùn)的是擅威,Scripting Bridge為Cocoa應(yīng)用提供了更友善的編程接口。
Cocoa Scripting Bridge
為了使用Scripting Bridge與應(yīng)用進(jìn)行交互冈钦,首先要先添加一個(gè)編程接口:
$ sdef /Applications/Safari.app | sdp -fh--basename Safari
sdef為應(yīng)用生成腳本定義文件郊丛。這些文件可以以管道輸入道sdp并格式轉(zhuǎn)成(在這里是)C頭文件。這樣的結(jié)果是添加該頭文件到應(yīng)用工程并提供第一類對(duì)象接口瞧筛。
這里舉個(gè)栗子來解釋如何使用Cocoa Scripting Bridge:
#import "Safari.h"SafariApplication *safari = [SBApplicationapplicationWithBundleIdentifier:@"com.apple.Safari"];for(SafariWindow *windowinsafari.windows) {if(window.visible) {window.currentTab.URL = [NSURLURLWithString:@"http://nshipster.com"];break;? ? }}
對(duì)比AppleScript上面顯得冗繁了點(diǎn)厉熟,但是卻更容易集成到已存在的代碼中去。在可讀性上更優(yōu)因?yàn)楫吘归L得更像OC较幌。
唉揍瑟,AppleScript的星芒也正出現(xiàn)消退,在最近發(fā)布的OS X與iWork應(yīng)用證答復(fù)減少它的戲份绅络。從這點(diǎn)說月培,未必值得在你的應(yīng)用中去添加這項(xiàng)(腳本)支持。
Pasteboard
剪貼板是OS X與iOS最常見的進(jìn)程間通信機(jī)制恩急。當(dāng)用戶跨應(yīng)用拷貝了一段文字杉畜,圖片,文檔衷恭,這時(shí)候通過mach port的com.apple.pboard服務(wù)媒介進(jìn)行從一個(gè)進(jìn)程到另一個(gè)進(jìn)程的數(shù)據(jù)交換此叠。
OS X上是NSPasteboard,iOS上對(duì)應(yīng)的是UIPasteboard随珠。它們幾乎是別無二致灭袁,但盡管大致一樣,對(duì)比OS X iOS上提供了更簡潔窗看,更現(xiàn)代化卻又不影響功效的API茸歧。
編寫剪貼板代碼幾乎就跟在GUI應(yīng)用上使用Edit > Copy操作一樣簡單:
NSImage *image;NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];[pasteboard clearContents];[pasteboard writeObjects:@[image]];
因?yàn)榧糍N動(dòng)作太頻繁了,所以要確認(rèn)剪貼內(nèi)容是否是你(應(yīng)用)所需要得:
NSPasteboard*pasteboard = [NSPasteboardgeneralPasteboard];if([pasteboardcanReadObjectForClasses:@[[NSImageclass]]options:nil]) {NSArray*contents = [pasteboardreadObjectsForClasses:@[[NSImageclass]]options:nil];NSImage*image = [contents firstObject];}
XPC
XPC是SDK中最先進(jìn)的進(jìn)程間通訊技術(shù)显沈。它架構(gòu)之初的目的在于避免長時(shí)間得運(yùn)行過程软瞎,來適應(yīng)有限的資源逢唤,在可能運(yùn)行的時(shí)候才進(jìn)行初始化。把XPC納入應(yīng)用而不做任何事情的想法是不現(xiàn)實(shí)的鳖藕,但這樣提供了更好的進(jìn)程間的特權(quán)分離和故障隔離。
XPC作為NSTask替代品甚至更多只锭。
2011推出以來著恩,XPC為OS X上的應(yīng)用沙盒提供基礎(chǔ)設(shè)施喉誊,iOS上的遠(yuǎn)程試圖控制器裹驰,還有兩個(gè)平臺(tái)上的應(yīng)用擴(kuò)展隧熙。它還廣范圍的用在系統(tǒng)框架和第一方應(yīng)用:
$find /Applications-name \*.xpc
控制臺(tái)輸入上面的命令行你會(huì)知道XPC無處不在片挂。在一般應(yīng)用中同樣的情形也在發(fā)生躏敢,比如圖片或者視頻轉(zhuǎn)變服務(wù),系統(tǒng)調(diào)用讥脐,網(wǎng)頁服務(wù)加載,或是第三方的授權(quán)岖免。
XPC負(fù)責(zé)進(jìn)程間通訊的同時(shí)還負(fù)責(zé)該服務(wù)生命周期的管理。包括注冊(cè)服務(wù),啟動(dòng)赢赊,以及通過launchd解決服務(wù)之間的通訊玩讳。一個(gè)XPC服務(wù)可以根據(jù)需求地洞熏纯,或者在崩潰的時(shí)候重啟误窖,或者是空閑的時(shí)候終止。正因如此丙唧,服務(wù)可以完全被設(shè)計(jì)成無狀態(tài)的,以便于在運(yùn)行的任何時(shí)間點(diǎn)的突然終止都能做到影響不大溪厘。
作為被iOS還有OS X中backported所采用的安全模塊,XPC服務(wù)默認(rèn)運(yùn)行在最為嚴(yán)格的環(huán)境:不能訪問文件闺骚,不能訪問網(wǎng)絡(luò)僻爽,沒有根權(quán)限升級(jí)。任何能做的事情就是對(duì)照被賦予的白名單列表贾惦。
XPC可以被libxpc C API訪問胸梆,或者是NSXPCConnection OC API敦捧。【注:作者會(huì)用低級(jí)API去實(shí)現(xiàn)(純C)】
XPC服務(wù)要么存在于應(yīng)用的沙盒中亦或是使用launchd調(diào)用跑在后臺(tái)碰镜。
服務(wù)調(diào)用帶事件句柄的xpc_main來獲取新的XPC連接兢卵。
staticvoidconnection_handler(xpc_connection_t peer){? ? xpc_connection_set_event_handler(peer, ^(xpc_object_tevent) {? ? ? ? peer_event_handler(peer,event);? ? });? ? xpc_connection_resume(peer);}intmain(intargc,constchar*argv[]){? xpc_main(connection_handler);? exit(EXIT_FAILURE);}
每個(gè)XPC連接是一對(duì)一的,意味著服務(wù)在不同的連接進(jìn)行操作绪颖,每次調(diào)用xpc_connection_create就會(huì)創(chuàng)建一個(gè)新的鏈接秽荤。【注:類似BSD套接字中的API accept函數(shù)柠横,服務(wù)在單個(gè)文件描述符進(jìn)行監(jiān)聽來為范圍內(nèi)的鏈接創(chuàng)建額外描述符】:
xpc_connection_t c = xpc_connection_create("com.example.service", NULL);xpc_connection_set_event_handler(c, ^(xpc_object_tevent) {// ...});xpc_connection_resume(c);
當(dāng)一個(gè)消息發(fā)送到XPC鏈接窃款,將自動(dòng)的派發(fā)到一個(gè)由runtime管理的消息隊(duì)列中。當(dāng)鏈接的遠(yuǎn)端一旦開啟的時(shí)候牍氛,消息將出隊(duì)并被發(fā)送晨继。
每個(gè)消息就是一個(gè)字典,字符串key和強(qiáng)類型值:
xpc_dictionary_t message = xpc_dictionary_create(NULL,NULL,0);xpc_dictionary_set_uint64(message,"foo",1);xpc_connection_send_message(c, message);xpc_release(message)
XPC對(duì)象對(duì)下列原始類型進(jìn)行操作:
Data
Boolean
Double
String
Signed Integer
Unsigned Integer
Date
UUID
Array
Dictionary
Null
XPC提供了一個(gè)便捷的方法來從dispatch_data_t數(shù)據(jù)類型進(jìn)行轉(zhuǎn)換搬俊,這樣從GCD到XPC的工作流程就簡化了:
void*buffer;size_tlength;dispatch_data_tddata =? ? dispatch_data_create(buffer,? ? ? ? ? ? ? ? ? ? ? ? length,? ? ? ? ? ? ? ? ? ? ? ? DISPATCH_TARGET_QUEUE_DEFAULT,? ? ? ? ? ? ? ? ? ? ? ? DISPATCH_DATA_DESTRUCTOR_MUNMAP);xpc_object_txdata = xpc_data_create_with_dispatch_data(ddata);
服務(wù)注冊(cè)
XPC可以注冊(cè)成啟動(dòng)項(xiàng)任務(wù)紊扬,配置成匹配IOKit事件自動(dòng)啟動(dòng),BSD通知或者是CFDistributedNotifications悠抹。這些標(biāo)準(zhǔn)都指定在服務(wù)的launchd.plist文件里:
.launchd.plist
LaunchEventscom.apple.iokit.matchingcom.example.device-attachidProduct2794idVendor725IOProviderClassIOUSBDeviceIOMatchLaunchStreamProcessTypeAdaptive
最近一次對(duì)于launchd屬性列表的修改是增加了ProcessTypeKey珠月,其用來在高級(jí)層面上描述啟動(dòng)機(jī)構(gòu)的預(yù)期目的。根據(jù)預(yù)描述行為期望楔敌,操作系統(tǒng)會(huì)響應(yīng)調(diào)整CPU和I/O的閾值。
為了注冊(cè)一個(gè)服務(wù)運(yùn)行大概五分鐘的時(shí)間驻谆,一套標(biāo)準(zhǔn)需要傳送給xpc_activity_register:
xpc_object_t criteria = xpc_dictionary_create(NULL,NULL,0);xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_INTERVAL,5*60);xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_GRACE_PERIOD,10*60);xpc_activity_register("com.example.app.activity",? ? ? ? ? ? ? ? ? ? ? criteria,? ? ? ? ? ? ? ? ? ? ? ^(xpc_activity_t activity){// Process Dataxpc_activity_set_state(activity, XPC_ACTIVITY_STATE_CONTINUE);dispatch_async(dispatch_get_main_queue(), ^{// Update UIxpc_activity_set_state(activity, XPC_ACTIVITY_STATE_DONE);? ? });});