进程间通讯 (OSX/iOS)

原文连接:http://nshipster.com/inter-process-communication/编程

总起

OS X是MacOS与NeXTSTEP的结合。OC是Smalltalk类面向对象编程与C的结合。iCloud则是苹果移动服务与云平台的结合。windows

上述都是一些亮点,可是不得不说苹果技术中的进程通信走的是“反人类”的道路。设计模式

因为不是根据每一个节点上最优原则进行设计,苹果的进程间通讯解决方案更显得混乱扎堆。结果是,大量重叠,不兼容的IPC技术在各个抽象层随处可见。(除了GCD还有剪贴板)安全

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

从低级内核抽象到高级,面向对象的API,它们都有各自特殊的表现以及安全特性。可是基础层面来看,它们都是从不一样上下文段传递或者获取数据的机制。网络

分述

Mach Ports

全部的进程间通信最终落实依赖的仍是Mach内核API提供的功能。架构

Mach端口是轻量而且强大的而又缺乏相关文档晦涩使用的(天使与恶魔)。app

经过一个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) {
    // ...
}

(消息)接收端稍微轻松点,由于消息只须要被声明而不用初始化:async

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);
    });
});
相关文章
相关标签/搜索