程序的安全稳定是越来越重要,特别在研发的初期设计阶段就要开始考虑了,需要很多方面来保障。本文介绍一种XPCService, macOS 平台下的多进程基础通讯,它拥有的特性能让应用变得更加稳定和安全。
XPC Service 概览
什么是 XPC Serives
XPC Serives 是libSystem的一部分,提供一个轻量级的进程间的基础通讯,它同时也结合了 GCD 和 launchd。能利用它来开发一套辅助你主应用App的程序,从而实现更加稳定和安全的效果。
为什么要用 XPC Service
官方介绍提到,选择使用 XPC Service 的两个主要原因:稳定和安全,为什么能带来稳定和安全呢,核心就在 XPC Service 独立进程和独立的沙盒机制的特性带来的。结合以下一些设想的场景,我们如果开发中结合了 XPC Service 的设计,那么能获取这些好处:
一个支持用户自定义插件的应用
插件的稳定性往往是把握在插件的开发者身上,这对主App来说,就属于不可控的影响稳定性的因素。这个时候,我们可以设计一个 plugin.xpc 来管理插件,它是一个完成独立的进程,假如用户自定的插件发生异常,导致Crash,这个时候并不会影响到主App的运行,主App还可以重新启动这个plugin.xpc的服务。主App比不使用XPC的设计,提高了稳定性。
对安全隐私敏感的应用
如果我们把一个应用利用XPC和Sandbox更加精细地设计,功能分布在不同的可执行文件和不同的沙盒环境下,对于一些攻击,网络破解应用的人而言,增加了不少难度。例如,一个照片的编辑软件,它通常不需要网络的权限,然而,如果这个应用有上传功能的模块,把它单独抽出来作为一个 XPC Service,启用沙盒,在 entitlement 中声明使用网络功能。这样就能做到权限分离,如果单纯使用编辑的用户是不会开启网络的功能的。
没办法沙盒化的应用
有些情况下的App是没办法沙盒化的,比如:App中需要调用系统命令行工具的,因为命令行的工具不在沙盒的环境中,对整个App沙盒化会导致调用失败。可能就因为这个小功能,放弃了对整个App的沙盒化。这个时候就可以把 XPC 利用起来,把不能沙盒化的那部分挪到 XPC 中去,XPC 关闭沙盒功能,App 就能正常开启沙盒功能了。
需要注意,虽然 APP 沙盒化了,但 XPC 没有开启沙盒,这样仍然是不能提交到App Store的。
处理开销大低频的任务
处理一些繁重且低频的任务,例如一些加密解密的工作,可能工作的过程中需要比较大的开销,使用 xpc 来抽离的话,工作完后就直接释放出这些资源。
XPC Service 快速搭建
通过 Xcode 的模板能够快速地创建一个 XPC Service,你可以选择(OC、Swift)Xcode 14 已经支持直接创建 Swift 的模版代码。OC和Swift 都是基于NSXPCConnection这个上层框架封装的接口,是官方比较推荐的做法,使用起来比较简单。同时,如果需要,也可以使用C-Base的接口,这个使用起来就会有一些成本了,但是会更加直观地理解XPC,后面也有一些用法示例。
创建
通过 Xcode 的模版能够快速地创建一个 XPC Service,目前 Xcode 14 已经支持直接创建 Swift 的模版代码,模板创建后有三个文件main.swift XPCService.swift XPCServiceDelegate.swift
第一步:初始化一个Listener,并为Service创建一个代理对象。如下图所示:

第二步:在ServiceDeleate 的回调接口 listener 中指定 exported 的接口(L8)和对象(L12),如下图

第三步: 声明 XPC Service 的 Exported 接口,如下图

第四步: 实现 Export 对象的具体逻辑

调用 XPC Service
官方的简单地实现了一个大小写的转换,调用 XPC 只需要在 主App 中添加下面的代码即可。

应用场景下实战
设计方案
我们设计一个简单的App,功能是支持用户选择电脑硬盘里的图片进行查看,可以选择性的进行裁剪(利用命令行工具Sips)然后将文件批量压缩打包,提交上传到后台。
往往商业的App可能功能会更加丰富,但可能抽象出来的几个方面都是一样的,磁盘读写、图片处理、数据处理、网络。通常我们的项目中会以模块的方式去划封这几层功能,整体项目编译成为一个App(可执行文件),如下图中的左侧ALL IN ONE所示,这种做法简单直接,没有沙盒化的程序没有任何限制。虽然简单,但同时在理论上,也存在稳定性和安全方面的隐患,那么,接下来想要介绍的就是如何利用XPC Service追求更加稳定安全的设计。
如下图Separate By XPC Service 所示,和左侧的图不一样,这里拆分了三个XPC Service出来,其中 Command Line Service 没有沙盒化,因为需要访问系统目录下()的工具,沙盒化会导致没有访问权限,例外两个ZipService 和 LeanCloudService 包括 Main App都有沙盒化,权限方面(entitilemensts),Main App有访问用户磁盘的权限,LeanCloudService有网络范围的权限,而Zip Service没有任何特殊权限。
主App 与各个 XPC Service 模块的概览:

这么拆分的设计,主要是有这些目的和好处:
- CommandLine Service 需要调用命令行工具,拆分出来后就不影响到Main Application 沙盒化,享受沙盒后的特性。同时,如果需要提交App Store,也只要把这个独立功能卸掉就行。
- 权限分享,主App只拥有读写用户选择的目录权限,网络权限收拢到
LeanCloudService的XPC中,主App在运行中是没有网络权限,一定程度保护了就是app运行时被篡改后上传数据的风险,提升了主App的安全性。 - 将数据的压缩处理放到
ZipService处理,它没有特殊的权利,职责单一,可以处理比较耗时的操作,抽成XPC而不是子线程处理后,它是一个独立的进程,就算运行过程中意外的Crash也不影响到主App的运行,从而提升了主App的稳定性。
关键技术点介绍
XPC Service 通过XCode 创建的模板的基本使用并不复杂,但是在具体开发的过程中遇到的实际问题处理起来可没那么简单,另一方面资料也比较少。讲如何一步步创建App篇幅也太冗长,所以,这里抽取一些重要或者比较复杂的问题进行展开的介绍。
沙盒与权限的设置
这块通过XCode直接配置很简单,重要的还是理解沙盒的机制和权限的这些特性。
MainApplication 支持写入用户选择的目录
它的 Entitlement.plist 文件的描述如下,意味着沙盒化,对用户选择的目录文件有读写权限。

LeanCloudService独享网络权限
它的 Entitlement.plist 文件的描述如下,意味 Fetch Service 沙盒化,并且拥有访问网络的权限。

关于 XPC Service 获取用户隐私信息的问题
XPC 具备沙盒的机制,也能勾选一下的权限,获取 App Data

但是这些权限的授予都是以 App 为单位的,不存在单独为 XPC 授权,应用场景可能在Agent或 Daemon的XPC模式下。
双向通讯
从模板生成出来的代码,只是一个单向的通信(Main App -> XPCService)但在实际开发的过程中单向通讯满足不了我们的需求,比如在这个App中,Main App 发送请求给 LeanCloudService上传文件,在上传的过程中LeanCloudService需要把上传的进度传递给Main App,由它来更新界面的进度条。
Listener & Connection
在展示如何建立双向通讯之前,需要先了解一下不同的Listener Connection
NSXPCListener 有三种不同创建方式应用于不同的场景:
open class func service() -> NSXPCListener对于XPCService(在App的bundle下面的XPCServices文件夹中的可执行文件)使用下面的这种方法创建一个Listener。open class func anonymous() -> NSXPCListener对于Main Application 来说,它不是Bundle OS Type code为XPC!类型的可执行文件,也不是一个Agent或Daemon,最合适的就是 anonymous了。public init(machServiceName name: String)如果是一个agent或daemon的可执行文件,它的运行方式由launchd管理(launchd.plist)这个不是在本文章涉及的,不展开介绍。
接着只要在 XPCService 侧创建Connection: 通过:
public init(listenerEndpoint endpoint: NSXPCListenerEndpoint)
创建一个NSXPCConnection 对象。
Main Application 把 endpoint 传递给 XPCService
建立的过程

核心代码片段:

这样,LeanCloudService 与 MainApplication 创建了双向的通讯了,创建Connection 和 Listener 方式都一样,唯一区别就是 Anonymous 和通过 endpoint 传递。
数据传递
与 XPCServices 进程间的数据传递是一个比较重要的问题,通过模板构造的能看到的只是简单的一些基础类型的传递,但其实进程间数据的传递有些持久化的属性,所以它拥有一些专属的类型。例如:file descriptors 、connection、endpoint ...
C Base API 方式
XPCService 除了使用 XPCConnection 上层封装的接口外,还能使用底层的(C-Based)XPC service,因为底层的 API 能够更加直观地看到 XPC Connection 支持的类型,所以我们可以先了解一下(C-Based)下的数据传递。
首先,使用基于C的 XPCService API时,有一重要对象需要了解一下
xpc_object_t
任何 XPC 的对象都可以处理为不透明的类型 xpc_object_t ,具体可以通过 man xpc_objects 文档中描述支持的函数来操作。

传递 file descriptors
我们的Main Application 和 XPCServices 只要沙盒化了之后,就是不能访问非沙盒外的文件的。在我们的这个文章中,Main Application 由用户选择了一个文件,这个文件的路径目录是在沙盒外的,我们 App 的下一步需要把文件交给 ZipService 进行压缩,那么,安装一般思路,传递给路径给它肯定是不行的,比如,用户选择压缩的文件路径是在 ~/Desktop/test.png ,对于ZipService 它是没有权限读取这个文件的。我们总不能再在进程间传递一个文件的data 数据,解决这种情况就是要用到 file descriptors 这个专属的类型。
XPC 对象类型:
类型 | 意义 |
|---|---|
fd | 文件描述符,客户程序可以通过调用 |
我们可以通过传递 fd 这种类型,从而实现从Main Application打开一个文件,获取它的文件描述符,然后传递给XPC Service 通过xpc_dictionary_set_fd(),XPC Service 拿到 fd通过xpc_dictionary_get_fd()
Main Application 侧通过 open() 获取到 fd ,然后通过xpc_dictionary_set_fd()放到message中,如下的代码片段:

XPC Service 侧通过xpc_dictionary_get_fd() 拿到了fd(L3),接着通过 read()(L10) 就可以获取到文件的内容了,如下代码片段:

NSXPCInterface 方式
官方的推荐是使用更上一层的API,上述描述的传递 file descriptors 对象,从 macOS 的版本 10.15 后,能使用 NSXPCInterface 的API 来传递xpc_object_t对象了。具体如何操作:
第一步: 先在接口中声明这个对象,如下图:

upload() 这个函数声明第一个参数为 xpc_object_t类型的参数。
第二步:调用 NSXPCInterface SetType 接口,需要注意这里两侧都得进行设置,Listener&Connection 两侧,如下图:
Listener 侧

Connection 侧

两侧都必须对应调用, 注意 argumentIndex 是从0开始,如果不是reply回调中的参数设置 false

剥离不能沙盒化的功能
sips 是macos 下自带的简单的图片处理工具,我们想要把它集成到App中,直接地在代码中调用命令行工具,实现简单的图片处理,但面临的问题是 sips 是安装在/usr/bin/sips,Main Application 必须取消沙盒化后才能调用/usr/bin目录下的系统工具,其实我们并不想为了这个小功能把整个app取消掉沙盒化,这个时候,就可以考虑把这个独立的功能放到一个非沙盒化的XPCService 中去
调用命令行工具
去掉沙盒机制非常简单,只需要在 Xcode 中的Capability 删掉 Sandbox 即可。
调用命令行的工具使用 Process() 即可,如下图所示

如果只是单纯访问固定的非沙盒路径的,也可以沙盒化,官方支持设置 temporary-exception
Entitlement key | Capability |
|---|---|
com.apple.security.temporary-exception.files.home-relative-path.read-only | 开启对于home指定子目录或者文件的只读权限 |
com.apple.security.temporary-exception.files.home-relative-path.read-write | 开启对于home指定子目录或者文件的读写权限 |
com.apple.security.temporary-exception.files.absolute-path.read-only | 开启对于 / 指定子目录或者文件的只读权限 |
com.apple.security.temporary-exception.files.absolute-path.read-write | 开启对于 / 指定子目录或者文件的读写权限 |
如下,设置需要访问 /usr/local/share/的例子。

不过对于需要访问到temporary-exception的功能最好也抽离出来到独立的XPCServices,说不定哪一天苹果就把这个功能给关了了呢,毕竟是Temporary。
总结
文章通过简单地介绍XPCServices到实践中遇到的比较重要的技术点,以一些场景来具化它在实际中的用法,包括基于C接口的一些用法。应该基本上覆盖了XPCService的用法了,对于XPCService比较熟悉了,如果开发的不是一个有UI的应用,而是一个XPC后台程序,Agent或者是Daemon。基本上开发都是一致的,只是变了由launchd显式管理而已。XPCService 还有其它的一些应用场景,大家可以继续探索!


