程序的安全稳定是越来越重要,特别在研发的初期设计阶段就要开始考虑了,需要很多方面来保障。本文介绍一种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 还有其它的一些应用场景,大家可以继续探索!