1. 程式人生 > >ios程序間通訊問題之一--程序間通訊 (OSX/iOS)

ios程序間通訊問題之一--程序間通訊 (OSX/iOS)

總起

OS X是MacOS與NeXTSTEP的結合。OC是Smalltalk類面向物件程式設計與C的結合。iCloud則是蘋果移動服務與雲平臺的結合。

上述都是一些亮點,但是不得不說蘋果技術中的程序通訊走的是“反人類”的道路。

由於不是根據每個節點上最優原則進行設計,蘋果的程序間通訊解決方案更顯得混亂扎堆。結果是,大量重疊,不相容的IPC技術在各個抽象層隨處可見。(除了GCD還有剪貼簿)

  • Mach Ports
  • Distributed Notifications
  • Distributed Objects
  • AppleEvents & AppleScript
  • Pasteboard
  • XPC

從低階核心抽象到高階,面向物件的API,它們都有各自特殊的表現以及安全特性。但是基礎層面來看,它們都是從不同上下文段傳遞或者獲取資料的機制。

分述

Mach Ports

所有的程序間通訊最終落實依賴的還是Mach核心API提供的功能。

Mach埠是輕量並且強大的而又缺少相關文件晦澀使用的(天使與惡魔)。

通過一個Mach埠傳送一個訊息呼叫一次mach_msg_send方法,但是這裡需要做一些配置來構建待發送的訊息:

natural_t data;
mach_port_t port;

struct {
    mach_msg_header_t header;
    mach_msg_body_t body;
    mach_msg_type_descriptor_t type;
} 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) { // ... }

(訊息)接收端稍微輕鬆點,因為訊息只需要被宣告而不用初始化:

mach_port_t port;

struct {
    mach_msg_header_t header;
    mach_msg_body_t body;
    mach_msg_type_descriptor_t type;
    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;
    // ...
}

還算不錯的是,Core FoundationFoundation為Mach埠提供了高階API。在核心基礎上封裝的CFMachPort / NSMachPort可以用做runloop源,儘管CFMachPort / NSMachPort有利於的是兩個不同埠之間的通訊同步。

CFMessagePort確實非常適合用於簡單的一對一通訊。簡簡單單幾行程式碼,一個本地埠就被附屬到runloop源上,只要獲取到訊息就執行回撥。

static CFDataRef 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);

若要進行傳送資料同樣也十分直截了當。只要完成指定遠端的埠,裝載資料,還有設定傳送與接收的超時時間的操作。剩下就由CFMessagePortSendRequest來接管了。

CFDataRef data;
SInt32 messageID = 0x1111; // Arbitrary
CFTimeInterval 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中有很多種兩個物件進行通訊的途徑。

當然也能進行直接訊息傳遞。也有像目標-動作,代理,回撥這些解耦,一對一的設計模式。KVO允許讓很多物件訂閱一個事件,但是它把這些物件都聯絡起來了。另一方面通知讓訊息全域性廣播,並且讓有監聽該廣播的物件接收該訊息。【注:想知道發了多少次廣播嗎?新增 NSNotificationCenter addObserverForName:object:queue:usingBlock,其中name與object置nil,看block被呼叫了幾次。】

每個應用為基礎應用訊息釋出-訂閱對自身通知中心例項進行管理。但是鮮有人知的APICFNotificationCenterGetDistributedCenter的通知可以進行系統級別範圍的通訊。

為了獲取通知,新增所要指定監聽訊息名的觀察者到通知釋出中心,當訊息接收到的時候函式指標指向的函式將被執行一次:

static void Callback(CFNotificationCenterRef center,
                     void *observer,
                     CFStringRef name,
                     const void *object,
                     CFDictionaryRef userInfo)
{
    // ...
}

CFNotificationCenterRef distributedCenter =
    CFNotificationCenterGetDistributedCenter();

CFNotificationSuspensionBehavior behavior =
        CFNotificationSuspensionBehaviorDeliverImmediately;

CFNotificationCenterAddObserver(distributedCenter,
                                NULL,
                                Callback,
                                CFSTR("notification.identifier"),
                                NULL,
                                behavior);

傳送端程式碼更為簡單,只要配置好ID,物件還有user info

void *object;
CFDictionaryRef userInfo;

CFNotificationCenterRef distributedCenter =
    CFNotificationCenterGetDistributedCenter();

CFNotificationCenterPostNotification(distributedCenter,
                                     CFSTR("notification.identifier"),
                                     object,
                                     userInfo,
                                     true);

連結兩個應用通訊的方式中,分發式通知是最為簡單的。用它來進行大量資料的傳輸是不明智的,但是對於輕量級資訊同步,分發式通知堪稱完美。

Distributed Objects

90年代中NeXT全盛時期,分發式物件(DO)是Cocoa框架中一個遠端訊息傳送特性。儘管現在已經不再大範圍的使用,在現代奇數層上IPC無障礙通訊仍然並未實現。

使用DO分發一個物件僅僅是搭建一個NSConnection並將其註冊為特殊(你分的清楚)的名字:

@protocol Protocol;

id <Protocol> vendedObject;

NSConnection *connection = [[NSConnection alloc] init];
[connection setRootObject:vendedObject];
[connection registerName:@"server"];

另外一個應用將會也建立同樣名字的並註冊過的連結,然後立即獲取一個原子代理當做原始物件。

id proxy = [NSConnection rootProxyForConnectionWithRegisteredName:@"server" host:nil];
[proxy setProtocolForProxy:@protocol(Protocol)];

只要分發物件代理收到訊息了,一個通過NSConnection連線遠端呼叫(RPC)將會根據傳送物件進行對應的計算並且返回結果給代理。【注:原理是一個OS管理的共享的NSPortNameServer例項對這個帶著名字的連線進行管控。】

分發式物件簡單,透明,健壯。簡直就是Cocoa中的標杆。。。

實際上,分散式物件不能像區域性物件那樣使用,那就是因為任何傳送給代理的訊息都可能丟擲異常。不想其他語言,OC沒有異常處理控制流程。所以對任何東西都進行@try/@catch也算是Cocoa大會很淒涼的補救了。

DO還有一個原因致其使用不便。在試圖通過連線“marshal values”時,物件和原語的差距尤為明顯。
此外,連線是完全加密的,和下方通訊通道擴充套件性的缺乏致使其在大多數的使用中通訊被迫中斷。

下方是左列分散式物件用來指定其屬性代理行為和方法引數的註解:

  • in:輸入引數,後續不再引用
  • out:引數被引用作為返回值
  • inout:輸入引數,引用作為返回值
  • const:常量引數
  • oneway:無障礙結果返回
  • bycopy:返回物件的拷貝
  • byref:返回物件的代理

AppleEvents & AppleScript

AppleEvents是經典Macintosh作業系統最持久的遺產。在System 7推出的AppleEvents允許應用程式在本地使用AppleScript或者使用程式連結的功能進行程式控制。現在AppleScript使用Cocoa Scripting Bridge,仍然是OS X應用程序間最直接的互動方式。【注:Mac系統的蘋果時間管理中心為AppleEvents提供了原始低階傳送機制,但是是在OS X的Mach埠基礎之上的重實現】。

也就是說,使用起來這是簡單而又古怪的技術之一。

AppleScript使用自然語言語法,設計初衷是沒有涉及引數而更容易掌握。雖然與人交流更親和了,但是寫起來確實噩夢。

為了更好的瞭解人類自然性,這裡有個栗子教你怎麼讓Safari在最前的視窗的啟用欄開啟一個URL。

tell application "Safari"
  set the URL of the front document to "http://nshipster.com"
end tell

在大部分情況下,AppleScript的語法自然語言的特性更多是不便不是優勢。(吐槽。。。略略略)

即便是經驗老道的OC開發者,不靠文件或者栗子寫出AppleScript是不可能的任務。

幸運的是,Scripting Bridge為Cocoa應用提供了更友善的程式設計介面。

Cocoa Scripting Bridge

為了使用Scripting Bridge與應用進行互動,首先要先新增一個程式設計介面:

$ sdef /Applications/Safari.app | sdp -fh --basename Safari

sdef為應用生成指令碼定義檔案。這些檔案可以以管道輸入道sdp並格式轉成(在這裡是)C標頭檔案。這樣的結果是新增該標頭檔案到應用工程並提供第一類物件介面。

這裡舉個栗子來解釋如何使用Cocoa Scripting Bridge

#import "Safari.h"

SafariApplication *safari = [SBApplication applicationWithBundleIdentifier:@"com.apple.Safari"];

for (SafariWindow *window in safari.windows) {
    if (window.visible) {
        window.currentTab.URL = [NSURL URLWithString:@"http://nshipster.com"];
        break;
    }
}

對比AppleScript上面顯得冗繁了點,但是卻更容易整合到已存在的程式碼中去。在可讀性上更優因為畢竟長得更像OC。

唉,AppleScript的星芒也正出現消退,在最近釋出的OS X與iWork應用證答覆減少它的戲份。從這點說,未必值得在你的應用中去新增這項(指令碼)支援。

Pasteboard

剪貼簿是OS X與iOS最常見的程序間通訊機制。當用戶跨應用拷貝了一段文字,圖片,文件,這時候通過mach portcom.apple.pboard服務媒介進行從一個程序到另一個程序的資料交換。

OS X上是NSPasteboard,iOS上對應的是UIPasteboard。它們幾乎是別無二致,但儘管大致一樣,對比OS X iOS上提供了更簡潔,更現代化卻又不影響功效的API。

編寫剪貼簿程式碼幾乎就跟在GUI應用上使用Edit > Copy操作一樣簡單:

NSImage *image;

NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
[pasteboard writeObjects:@[image]];

因為剪貼動作太頻繁了,所以要確認剪貼內容是否是你(應用)所需要得:

NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];

if ([pasteboard canReadObjectForClasses:@[[NSImage class]] options:nil]) {
    NSArray *contents = [pasteboard readObjectsForClasses:@[[NSImage class]] options: nil];
    NSImage *image = [contents firstObject];
}

XPC

XPC是SDK中最先進的程序間通訊技術。它架構之初的目的在於避免長時間得執行過程,來適應有限的資源,在可能執行的時候才進行初始化。把XPC納入應用而不做任何事情的想法是不現實的,但這樣提供了更好的程序間的特權分離和故障隔離。

XPC作為NSTask替代品甚至更多。

2011推出以來,XPC為OS X上的應用沙盒提供基礎設施,iOS上的遠端試圖控制器,還有兩個平臺上的應用擴充套件。它還廣範圍的用在系統框架和第一方應用:

$ find /Applications -name \*.xpc

控制檯輸入上面的命令列你會知道XPC無處不在。在一般應用中同樣的情形也在發生,比如圖片或者視訊轉變服務,系統呼叫,網頁服務載入,或是第三方的授權。

XPC負責程序間通訊的同時還負責該服務生命週期的管理。包括註冊服務,啟動,以及通過launchd解決服務之間的通訊。一個XPC服務可以根據需求地洞,或者在崩潰的時候重啟,或者是空閒的時候終止。正因如此,服務可以完全被設計成無狀態的,以便於在執行的任何時間點的突然終止都能做到影響不大。

作為被iOS還有OS X中backported所採用的安全模組,XPC服務預設執行在最為嚴格的環境:不能訪問檔案,不能訪問網路,沒有根許可權升級。任何能做的事情就是對照被賦予的白名單列表。

XPC可以被libxpc C API訪問,或者是NSXPCConnection OC API。【注:作者會用低階API去實現(純C)】

XPC服務要麼存在於應用的沙盒中亦或是使用launchd呼叫跑在後臺。

服務呼叫帶事件控制代碼的xpc_main來獲取新的XPC連線。

static void connection_handler(xpc_connection_t peer) {
    xpc_connection_set_event_handler(peer, ^(xpc_object_t event) {
        peer_event_handler(peer, event);
    });

    xpc_connection_resume(peer);
}

int main(int argc, const char *argv[]) {
   xpc_main(connection_handler);
   exit(EXIT_FAILURE);
}

每個XPC連線是一對一的,意味著服務在不同的連線進行操作,每次呼叫xpc_connection_create就會建立一個新的連結。【注:類似BSD套接字中的API accept函式,服務在單個檔案描述符進行監聽來為範圍內的連結建立額外描述符】:

xpc_connection_t c = xpc_connection_create("com.example.service", NULL);
xpc_connection_set_event_handler(c, ^(xpc_object_t event) {
    // ...
});
xpc_connection_resume(c);

當一個訊息傳送到XPC連結,將自動的派發到一個由runtime管理的訊息佇列中。當連結的遠端一旦開啟的時候,訊息將出隊並被髮送。

每個訊息就是一個字典,字串key和強型別值:

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物件對下列原始型別進行操作:

  • Data
  • Boolean
  • Double
  • String
  • Signed Integer
  • Unsigned Integer
  • Date
  • UUID
  • Array
  • Dictionary
  • Null

XPC提供了一個便捷的方法來從dispatch_data_t資料型別進行轉換,這樣從GCD到XPC的工作流程就簡化了:

void *buffer;
size_t length;
dispatch_data_t ddata =
    dispatch_data_create(buffer,
                         length,
                         DISPATCH_TARGET_QUEUE_DEFAULT,
                         DISPATCH_DATA_DESTRUCTOR_MUNMAP);

xpc_object_t xdata = xpc_data_create_with_dispatch_data(ddata);

服務註冊

XPC可以註冊成啟動項任務,配置成匹配IOKit事件自動啟動,BSD通知或者是CFDistributedNotifications。這些標準都指定在服務的launchd.plist檔案裡:
.launchd.plist

<key>LaunchEvents</key>
<dict>
  <key>com.apple.iokit.matching</key>
  <dict>
      <key>com.example.device-attach</key>
      <dict>
          <key>idProduct</key>
          <integer>2794</integer>
          <key>idVendor</key>
          <integer>725</integer>
          <key>IOProviderClass</key>
          <string>IOUSBDevice</string>
          <key>IOMatchLaunchStream</key>
          <true/>
          <key>ProcessType</key>
          <string>Adaptive</string>
      </dict>
  </dict>
</dict>

最近一次對於launchd屬性列表的修改是增加了ProcessType Key,其用來在高階層面上描述啟動機構的預期目的。根據預描述行為期望,作業系統會響應調整CPU和I/O的閾值。

為了註冊一個服務執行大概五分鐘的時間,一套標準需要傳送給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 Data

    xpc_activity_set_state(activity, XPC_ACTIVITY_STATE_CONTINUE);

    dispatch_async(dispatch_get_main_queue(), ^{
        // Update UI

        xpc_activity_set_state(activity, XPC_ACTIVITY_STATE_DONE);
    });
});