iOS 进程间通信翻译

前言

原文的作者是 AFNetworking 的作者 Mattt Thompson 大神,原文链接

正文

由于历史的机缘巧合,苹果通过技术的结合创造出了很多优秀的产品: OS X 是 MacOS 和 NeXTSTEP 的结合,Objective-C 是 Smalltalk 的面向对象语法和 C 的结合,iCloud 是 MobileMe 和 actual clonds 的结合。

尽管苹果通过这种完美的结合丰富了自己的技术栈,但进程间通信却是一个典型的反面例子。

苹果并没有直接采用最好的可行方案,而是有点堆砌解决方案的意思。这造成了苹果采用的多种进程间通信方案之间无法兼容,分散的堆砌在抽象层。尽管 OS X 支持了所有的进程间通信方案,但在 iOS 上仅支持 Grand Central Dispatch 和 剪贴板。

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

上面的方案从抽象的底层内核接口过度到高级的面向对象的接口,这些方案都有自己的性能和安全特性。无一例外的是这几种进程间通信技术背后的原理都是全局的上下文来发送接收数据。

Mach Ports

所有的进程间通信方案最终都是基于操作系统提供的很底层的内核接口 Mach ports。

虽然 Mach ports 是操作系统提供的轻量级功能强大的内核接口,但苦于该接口的文档有限。

向端口发送消息只需要调用方法 '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 Foundation 和 Foundation 两个框架对 Mach ports 进行了封装,提供了更高级的接口。
CFMachPort / NSMachPort 是对内核接口的封装可用于 NSRunLoop 来监听来自端口的消息,同时 CFMessagePort / NSMessagePort 可用于两个端口间的异步通信。

CFMessagePort 是一个很好的端对端通信方案。只需要简单的几行代码就可以为一个端口设置一个回调方法并让该端口作为 NSRunLoop 的消息源,当 NSRunLoop 接受到来自该端口的消息时便会执行为该端口设置的回调:

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 框架中有多种方式可用于对象间通信:

直接发送消息是一种。当然也可以使用 target-action, delegate, 和回调这些低耦合,一对一的通信方案。KVO 允许多个对象监听同一个事件,但这样造成了这些对象间产生了耦合。通知是另一种可以广播并且可以让任何想要监听的对象接收到消息的方式。

每个应用程序都维护了自己的通知中心用于发送接收通知。但这有一个很少被人熟知的 Core Foundation 接口 CFNotificationCenterGetDistributedCenter,该接口系统级的进程间通信。

为了监听通知,需要向通知中心添加一个申明了名字和回调函数指针的通知:

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

发送通知同样简单,只需要指定待发送的通知的标识符,附着于该通知的消息和用户信息即可:

void *object;
CFDictionaryRef userInfo;

CFNotificationCenterRef distributedCenter =
    CFNotificationCenterGetDistributedCenter();

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

在所有的应用程序间通信的方案中,发送通知是目前为止最简单的方式,其可以很好的胜任一些简单的工作如同步偏好设置或者触发数据的拉取,但不建议使用通知发送大量的数据。

Distributed Objects

Distributed Objects (DO) 是 Cocoa 的一种远程消息特性,其在90年代中期的 NeXT 系统中达到鼎盛。这种方式以及被很少使用了,但设计出精巧的进程间通信方案的梦想至今仍未实现。

使用 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)];
  • in: Argument is used as input, but not referenced later
  • out: Argument is used to return a value by reference
  • inout: Argument is used as input and returned by reference
  • const: Argument is constant
  • oneway: Return without blocking for result
  • bycopy: Return a copy of the object
  • byref: Return a proxy of the object

AppleEvents & AppleScript

AppleEvents 是经典的麦金塔操作系统中最重要的遗产。AppleEvents 在 System 7 中被引入,其允许在本地使用 AppleScript 或者远程使用叫 Program Linking 的特性来控制应用。到现在,使用 Cocoa Scripting Bridge 的 AppleScript 仍然是 OS X 上与应用交互最直接的方式。

AppleEvents 和 AppleScript 很容易成为最奇怪的技术之一。

AppleScript 使用更自然的语法,这对于没有非编程人员更容易接受。尽管 AppleScript 在易阅读方面取得了成功,但其书写起来却十分痛苦。

为了更好的理解其语法很自然的表达方式,下面给出一个在窗口活跃界面中使用 Safari 打开 URL 的例子:

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

AppleScript 自然的语法很多时候却成为了一种负担。英语和其他人类语言一样,通常在表达中会存在很多冗余。语言中的冗余对于人类来讲很容易接受,但对于计算机来说却是很困难的一件事。

幸好 Scripting Bridge 为 Cocoa 应用程序提供了较好的编程接口。

Cocoa Scripting Bridge

为了使用 Scripting Bridge 和应用程序交互,首先需要生成一个编程接口:

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

sdef 为应用生成了脚本定义。这些脚本文件通过管道(译注:管道是类 Unix 系统中的一种进程间通信的方式)发送到 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 稍微复杂些,但却更容易集成到现有的代码中。

Pasteboard

剪贴板是 OS X 和 iOS 上最直观的进程间通信方式。用户在 mach 端口上进行的两个应用间的文本,图片,文档的复制粘贴操作是通过 com.apple.pboard 服务进程完成的。

在 OS X 上使用 NSPasteboard 来完成复制粘贴操作,而 iOS 上使用 UIPasteboard。这两个类基本一样,但是如同两个平台上的其他类,iOS 相对 OS X 来说总是提供更简洁更现代化的接口。

编程实现复制粘贴功能和在应用程序用户界面中点击 编辑 > 拷贝 一样简单:

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];
}

粘贴板作为传输数据的方式最吸引人的地方在于其对拷贝的数据的多种呈现方式。例如,对文本的拷贝可以同时当做富文本 (RTF) 和纯文本 (TXT),即允许 WYSIWYG 编辑器呈现出富文本的样式,同事允许代码编辑器以纯文本的方式呈现出来。

这些呈现形式可以通过遵循 NSPasteboardItemDataProvider 协议来实现。遵循该协议后允许衍生出来的数据呈现形式,比如允许富文本在必要的时候可以转换为纯文本。

每种呈现形式都由唯一类型标识符 Unique Type Identifier (UTI) 表示,这个概念将在下一章中进行讨论。

XPC

SDKs 中的 XPC 是进程间通信的中最先进的。其架构目的是为了避免进程长时间运行,自适应可用的系统资源,延迟对象的初始化。把 XPC 集成到应用中可以为进程间通信提供很好的错误隔离。

XPC 可以做为 NSTask 的替换方案。

XPC 在2011年引入系统中为 OS X 带来了沙盒机制,为 iOS 带来了远程视图控制机制和应用扩展。其已被广泛应用在了系统框架和系统应用中:

$ find /Applications -name \*.xpc

通过对集成了 XPC 服务应用的统计,你可以对何时在自己的应用中集成 XPC 有更好的理解。例如应用中常用的图片视频转换服务,系统函数调用,web 服务和第三方的验证服务这些情况下都可以集成 XPC.

XPC 负责对进程间通信和系统服务的生命周期进行管理。注册服务,开启服务,和其他服务进行通信都是由 launchd (译注:可看做 daemon 守护进程)进行管理。XPC 服务可以按需启动或在崩溃后重新启动服务也可以在闲置时关闭该服务。因此,为了允许在执行过程中可以随时终止服务,服务应该被设计为无状态形式。

由于 iOS 和 OS X 采用新的安全策略,默认情况下 XPC 服务是在及其严格的环境中运行:无文件系统访问权限,无网络访问权限,无 root 权限。应用所具有的访问权限都需要加入一个白名单文件中。

可以通过 libxpc 的 C 接口或者 NSXPCConnection 的 Objective-C 接口使用 XPC 服务。XPC 服务可以在应用的 bundle 或者使用 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_t c = xpc_connection_create("com.example.service", NULL);
xpc_connection_set_event_handler(c, ^(xpc_object_t event) {
    // ...
});
xpc_connection_resume(c);
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 类型的数据:

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);
dispatch_queue_t queue;
xpc_connection_send_message_with_reply(c, message, queue,
    ^(xpc_object_t reply)
{
      if (xpc_get_type(event) == XPC_TYPE_DICTIONARY) {
         // ...
      }
});

Registering Services

XPC 也可以配置为监听到 IOKit 事件,BSD(译注:一种 Unix 发型版本) 通知或者 CFDistributedNotifications 时自动启动,并注册为 launchd 任务 (译注:开机启动的常驻系统服务)。这些功能可咋系统服务文件 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.plist 文件中新增了一个描述启动代理目的的键 ProcessType。基于设定的键值,操作系统可以自适应的使用的 CPU 和 I/O 宽带。

Process Types and Contention Behavior

Process Type Contention Behavior
Standard Default value
Adaptive Contend with apps when doing work on their behalf
Background Never contend with apps
Interactive Always contend with apps

为了注册一个每 5 分钟运行一次的服务,需要在 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);
    });
});

欢迎关注我的简书,我会定期做一些技术分享:)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 总起 OS X是MacOS与NeXTSTEP的结合。OC是Smalltalk类面向对象编程与C的结合。iCloud...
    Flighting拾壹狼阅读 5,949评论 3 8
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,868评论 6 13
  • Runloop是iOS和OSX开发中非常基础的一个概念,从概念开始学习。 RunLoop的概念 -般说,一个线程一...
    小猫仔阅读 979评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 转自http://blog.ibireme.com/2015/05/18/runloop 深入理解RunLoop ...
    飘金阅读 975评论 0 4