XPC 详解
监听者 (Listener),连接 (Connection) 和导出对象 (Exported Object)
在 App 端,我们有一个 connection 对象。每次将数据发给 service 时,我们需要调用 remoteObjectProxyWithErrorHandler
方法来创建一个远程对象代理 (remote object proxy)。
而在service端,则多了一层。首先需要一个 listener,用来监听来自 App 的传入 connection。App 可以创建多个 connection,listener 会在 service 端建立相应的 connection 对象。每个 connection 对象都有唯一的 exported object,在 App 端,通过 remote object proxy 发送的消息就是给它的。
当 App 创建一个到 XPC service 的 connection 时,是 XPC 在管理这个 service 的生命周期,service 的启动与停止都由 XPC runtime 完成,这对 App 来说是透明的。而且如果 service 因为某种原因 crash 了,也会透明地被重启。
App 初始化 XPC connection 的时候,XPC service 并不会启动,直到 App 实际发送的第一条消息到 remote object proxy 时才启动。如果当前没有未结束的响应,系统可能会因为内存压力或者 XPC service 已经闲置了一段时间而停止这个 service。这种情况下,App 持有的 connection 对象任然有效,下次再使用这个 connection 对象的时候,XPC 系统会自动重启对应的 XPC service。
如果 XPC service crash 了,它也会被透明地重启,并且其对应的 connection 也会一直有效。但是如果 XPC service 是在接收消息时 crash 了的话,App 需用重新发送该消息才能接受到对应的响应。这就是为什么要调用 remoteObjectProxyWithErrorHandler
方法来设置错误处理函数了。
这个方法接受一个闭包作为参数,在发生错误的时候被执行。XPC API 保证在错误处理里的闭包或者是消息响应里的闭包之中,只有一个会被执行;如果消息消息响应里的闭包被执行了,那么错误处理的就不会被执行,反之亦然。这样就使得资源清理变得容易了。
中断 (Interruption) 和失效 (Invalidation)
XPC 的最常见的用法是 App 发消息给它的 XPC service。XPC 允许非常灵活的设置。我们通过下文会了解到,connection 是双向的,它可以是匿名监听者 (anonymous listeners)。如果另一端消失了(因为 crash 或者是正常的进程终止),这时连接将很有可能变得无效。我们可以给 connection 对象设置失效处理函数,如果 XPC runtime 无法重新创建这个 connection,我们的失效处理函数将会被执行。
我们还可以给 connection 设置中断处理程序,会在 connection 被中断的时候会执行,尽管此时 connection 仍然是有效的。
在 NSXPCConnection中
对应的两个属性是:
1 | var interruptionHandler: (() -> Void)? { get set } |
双向连接 (Bidirectional Connections)
一个经常被忽略而又有意思的事实是:connection 是双向的。但是只能通过 App 创建到 service 的初始连接。service 不能主动创建到 App 的连接。一旦连接已经建好了,两端都可以发起请求。
正如 service 端给 connection 对象设置了 exportedObject
,App 端也可以这么做。这样可以让 service 端通过 remoteObjectProxy
来和 App 的 exported object
进行通信了。值得注意是,XPC service 由系统管理其生命周期,如果没有未完成的请求,可能会被停止掉。
服务查找 (Service Lookup)
当我们连接到 XPC service 的时候,我们需要找到连接的另一端。对于使用私有 XPC service 的 App,XPC 会在 App 的 bundle 范围内通过名字查找。还有其他的方法来连接到 XPC,让我们来看看所有的可能性。
XPC Service
假如 App 使用:
1 | NSXPCConnection(serviceName: "io.objc.myapp.myservice") |
XPC 会在 App 自己的命名空间 (namespace) 查找名为 io.objc.myapp.myservice
的service,这样的 service 仅对当前 App 有效,其他 App 无法连接。XPC service bundle 要么是位于 App 的 bundle 里,要么是在该 App 使用的 Framework 的 bundle 里。
Mach Service
另一个选择是使用:
1 | NSXPCConnection(machServiceName: "io.objc.mymachservice", options: NSXPCConnectionOptions(0)) |
这会在当前用户的登录会话 (login session) 中查找名为 io.objc.mymachservice
的service。 我们可以在 /Library/LaunchAgents
或 ~/Library/LaunchAgents
目录下安装 launch agent,这些 launch agent 也以与 App 里的 XPC service 几乎相同的方式来提供 service。由于 launch agent 会在 per-login session 中启动的,在同一个登录会话中运行的多个 App 可以和同一个 launch agent 进行通信。
这种方法很有用,例如 状态栏 (Status Bar) 中的 menu extra 程序(即右上角只有菜单项的 App)需要和 UI App 进行通信的时候。普通 App 和 menu extra 程序都可以和同一个 launch agent 进行通信并交互数据。当你需要让两个以上的进程需要相互通信,XPC 可以是一个非常优雅的方案。
假设我们要写一个天气类的 App,我们可以把天气数据的抓取和解析做成 launch agent 方式的 XPC service。我们可以分别创建 menu extra 程序,普通 App,以及通知中心的 Widget 来显示同样的天气数据。它们都可以通过 NSXPCConnection
和同一个 launch agent 进行通信。
与 XPC service 相同,launch agent 的生命周期也可以完全由 XPC 掌控:按需启动,闲置或者系统内存不足的时候停止。
匿名监听者 (Anonymous Listeners) 和端点 (Endpoints)
XPC 有通过 connection 来传递被称为 listener endpoints
的能力。这个概念一开始会让人非常费解,但是它可以带来更大的灵活性。
比如说我们有两个 App,我们希望它们能够过 XPC 来互相通信,每个 App 都不知道其他 App 的存在,但它们都知道相同的一个(共享)launch agent。
这两个 App 可以先连接到 launch agent。App A 创建一个被称为 匿名监听者 (anonymous listener) 的对象,并通过 XPC 发送一个 端点 (endpoint) ,并由匿名监听者创建的对象给 launch agent。App B 可以通过 XPC 在同样的 launch agent 中拿到这个 endpoint。这时,App B 就可以直接连接到这个匿名监听者,即 App A。
在 App A 创建一个 anonymous listener:
1 | let listener = NSXPCListener.anonymousListener() |
类似于 XPC service 创建普通的 listener。然后从这个 listener 创建一个 endpoint:
1 | let endpoint = listener.endpoint |
这个 endpoint 可以通过 XPC 来传递(实现了 NSSecureCoding
协议 )。一旦 App B 获取到这个 endpoint,它可以创建到 App A 的 listener 的一个 connection:
1 | let connection = NSXPCConnection(listenerEndpoint: endpoint) |
Privileged Mach Service
最后一个选择是使用:
1 | NSXPCConnection(machServiceName: "io.objc.mymachservice", options: .Privileged) |
这种方式和 launch agent 非常类似,不同的是创建了到 launch daemon 的 connection。launch agent 进程是 per user 的,它们以用户的身份运行在用户的登录会话 (login session) 中。守护进程 (Daemon) 则是 per machine 的,即使当前多个用户登录,一个 XPC daemon 也只有一个实例运行。
如果要运行 daemon 的话,有很多安全相关的问题需要考虑。虽然以 root 权限运行 daemon 是可能的,但是最好是不要这么这么做。我们可能更希望它以一些独特的用户身份来运行。具体可以参考 TN2083 - Designing Secure Helpers and Daemons 。大多数情况,我们并不需要 root 权限。