本文由 简悦 SimpRead 转码, 原文地址 rderik.com
在 macOS 中,我们可以使用许多进程间通信(IPC)机制。最新的机制之一是 XP......
我们可以使用 macOS 中的许多进程间通信(IPC)机制。最新的机制之一是 XPC1。在 XPC 之前,使用 IPC 并在进程间提供服务的常见方式是通过套接字或马赫消息(使用马赫端口)。
如今,Apple 鼓励我们在提供服务的应用程序和框架中使用 XPC。此外,苹果公司还在其代码中更新了许多框架、守护进程和应用程序,以便使用 XPC。
可以想象,XPC 是一个非常值得学习的工具。在本篇文章中,我们将学习
- 如何在我们的应用程序中使用 XPC,将其分解成小模块,提供仅在需要时才能请求的服务。想象一下,在一个应用程序中,有专门用于特定任务的代码。但该任务很少被使用。应用程序不需要始终将代码保存在内存中,这样会增加进程的内存占用。我们可以使用 XPC 服务(注意大写的 S)来提供只在需要时才执行的服务。此外,如果操作系统需要更多内存或进程闲置时间较长,也可以停止服务。
让我们来看看使用 XPC 可以实现哪些功能。
* 你可以在 GitHub 代码库 中查看完整代码。
*更新: 如果你对创建一个提供 XPC 服务的启动代理感兴趣,可以查看下面的帖子。
XPC 背后的理念及其用途
XPC 为进程间通信(Inter-Process-Communication)提供了一种新的抽象,这种简单的抽象本身就很方便(使用 MIG 就很复杂)。但除了拥有一个更易于使用的 IPC 机制外,XPC 实现还为我们带来了一些额外的好处。我们稍后会讨论这些好处,但首先,让我们来探讨一下 XPC 的一些用例。
用于通信进程的 XPC
如前所述,XPC 机制为 IPC 提供了套接字(或使用 "MIG "的马赫服务)的替代方案。例如,我们可以让一个进程充当 "服务器",等待客户访问它的 API 并提供某种服务。
想象一下,我们有一个处理用户联系人的应用程序。该应用程序提供了一个 API,您可以通过您的应用程序访问该 API 并查询用户联系人的详细信息。这个例子很容易成为由 AddressBook.app 提供的 XPC 服务。我们不直接访问 XPC 服务,而是使用 Contacts.framework提供的 API,但该框架是基于 XPC 服务的。
以这种方式使用的 XPC 服务是沟通不同进程的绝佳方式。XPC 中的数据交换是通过 "Plists "进行的,这也允许数据类型验证。
当我们谈到 XPC 服务(大写 "S")时,我们指的是名为 XPC 服务的捆绑包。苹果生态系统中的捆绑指的是由特定目录结构代表的实体。最常见的捆绑包是应用程序捆绑包。如果你右键单击任何应用程序(例如 "Chess.app")并选择 "Show content"(显示内容),你会发现一个目录结构。
回到 XPC,应用程序可以有多个 XPC 服务捆绑包。你可以在应用程序捆绑包内的 Contents/XPCServices/ 目录中找到它们。你可以在你的 /Applications 目录中搜索,看看有多少应用程序依赖于 XPC 服务。
$ find /Applications/ -name "*.xpc"
# a few results on my computer
/Applications//Safari.app/Contents/XPCServices/com.apple.Safari.SandboxBroker.xpc
/Applications//Safari.app/Contents/XPCServices/com.apple.WebKit.WebContent.Safari.xpc
/Applications//Safari.app/Contents/XPCServices/com.apple.Safari.BrowserDataImportingService.xpc
/Applications//Keynote.app/Contents/XPCServices/com.apple.iWork.ArchiveUpgrader.xpc
/Applications//Keynote.app/Contents/XPCServices/com.apple.iWork.ExternalResourceAccessor.xpc
每个应用程序内部的 XPC 服务提供的服务都可以从主 "应用程序 "轻松访问。你也可以在 "框架"(另一种捆绑包)中使用 XPC 服务。你可以查看计算机上的框架,以了解一些示例:
$ find /System/Library/Frameworks -name "*.xpc"
# a few results on my computer
/System/Library/Frameworks/MediaAccessibility.framework/Versions/A/XPCServices/com.apple.accessibility.mediaaccessibilityd.xpc
/System/Library/Frameworks/AudioToolbox.framework/XPCServices/RemoteProcessingBlockRegistrar.xpc
/System/Library/Frameworks/AudioToolbox.framework/XPCServices/CAReportingService.xpc
/System/Library/Frameworks/AudioToolbox.framework/XPCServices/com.apple.audio.SandboxHelper.xpc
/System/Library/Frameworks/AudioToolbox.framework/XPCServices/aupbregistrarservice.internal.xpc
/System/Library/Frameworks/AudioToolbox.framework/XPCServices/com.apple.audio.InfoHelper.xpc
/System/Library/Frameworks/AudioToolbox.framework/XPCServices/com.apple.audio.ComponentTagHelper.xpc
/System/Library/Frameworks/ColorSync.framework/Versions/A/XPCServices/com.apple.ColorSyncXPCAgent.xpc
XPC 服务的其他优势
在应用程序中使用 XPC 服务可以将某些功能分解为单独的模块(XPC 服务)。我们可以创建一个 XPC 服务,让它负责运行一些成本高但频率低的任务。例如,生成随机数的加密任务。
将我们的应用程序拆分成特定的服务,可以让我们的主应用程序更精简,运行时占用的内存也更少。只按需运行我们的 XPC 服务。
另一个额外的好处是,XPC 服务在自己的进程中运行。如果该进程崩溃或被杀死,也不会影响我们的主程序。想象一下,你的应用程序支持用户定义的插件。而这些插件是使用 XPC 服务构建的。如果它们的编码不完善而崩溃,也不会影响主程序的完整性。
XPC 服务的另一个好处是,它们可以拥有自己的权限。应用程序只有在使用 XPC 服务提供的需要权限的服务时才需要权限。想象一下,你有一个使用位置服务的应用程序,但只用于特定功能。你可以将这些功能转移到一个 XPC 服务中,并将位置权限添加到该 XPC 服务中。如果你的用户从不需要使用定位的功能,那么它就不会被提示获取权限,从而使你的应用程序更值得信赖。
以上就是使用 XPC 的一些好处。让我们看看 XPC 服务是如何管理的。
我们已经讨论过 XPC 服务可以按需运行,但谁负责管理你的 XPC 服务呢?
XPC 和我们的朋友 launchd
launchd 是我们系统上运行的第一个进程。它负责启动和管理其他进程、服务和守护进程。launchd 还负责调度任务。因此,"launchd "负责管理 XPC 服务也是合情合理的。
正如我之前提到的,如果我们的 XPC 服务长期闲置,它可以被停止,也可以按需生成。所有的管理都由 launchd 完成,我们不需要做任何事情就能让它工作。
launchd 拥有关于整个系统资源可用性和内存压力的信息,谁能比 launchd 更有效地利用系统资源呢?要我说,这是一个巧妙的实现。
好了,关于 XPC 的理论讲得够多了,让我们来做点实际的事情。
创建一个包含 XPC 服务的 macOS 应用程序
让我们创建一个使用 XPC 服务的应用程序。我们将使用一个基于代理的菜单栏应用程序。就像我们在"通过创建基于代理(菜单栏)的应用程序了解 macOS 应用程序的几个概念 "一文中创建的应用程序一样。
我们将使用 Swift 包管理器构建应用程序和 XPC 服务。如果您想了解如何在不使用 Xcode 的情况下创建应用程序,请查看该文章。
我们的 XPC 服务将提供一个获取公共 IP 的函数。我们将在 "菜单栏 "应用程序中添加新选项(新选项将调用我们的 XPC 服务)。
在进入 XPC 代码之前,我将稍微快一点,其余部分在另一篇文章中已经介绍过了。
首先创建目录并初始化 Swift 项目。名称并不重要。我将使用上述文章中的代码作为基础,因此我将项目命名为 "XPCSquirrel",因为之前的代码只有 "Squirrel":
$ mkdir XPCSquirrel
$ swift package init --type executable
正如我提到的,我们的代码将基于之前的 Squirrel 实现。我只想向你展示完整的文件,而不会详细解释它们是如何工作的,直到我们进入与 XPC 相关的代码。
main.swift "看起来像这样:
import Foundation
import Cocoa
let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
"AppDelegate.swift"将包含以下内容:
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
var statusBarItem: NSStatusItem!
var counter: Int = 0
func applicationDidFinishLaunching(_ aNotification: Notification) {
let statusBar = NSStatusBar.system
statusBarItem = statusBar.statusItem(
withLength: NSStatusItem.variableLength)
statusBarItem.button?.title = "🌰 \(counter)"
let statusBarMenu = NSMenu(title: "Counter Bar Menu")
statusBarItem.menu = statusBarMenu
statusBarMenu.addItem(
withTitle: "Increase",
action: #selector(AppDelegate.increaseCount),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Decrease",
action: #selector(AppDelegate.decreaseCount),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Quit",
action: #selector(AppDelegate.quit),
keyEquivalent: "")
}
@objc func showCounter() {
statusBarItem.button?.title = "🌰 \(counter)"
}
@objc func increaseCount() {
counter += 1
showCounter()
}
@objc func decreaseCount() {
counter -= 1
showCounter()
}
@objc func quit() {
NSApplication.shared.terminate(self)
}
}
我们将手动创建应用程序捆绑包和 XPC 服务捆绑包。这意味着我们将为每个捆绑包手动创建 Info.plists。我们将在名为 "SupportFiles "的目录中创建它们,然后将它们移动到应用程序捆绑包和 XPC 服务捆绑包的适当位置。
我们创建的是最基本的捆绑包,因此缺少很多信息。我们创建的应用程序捆绑包肯定不会获得 App Store 的批准。但为了了解一切是如何工作的,我们最好只看所需的信息。
创建一个文件 MainInfo.plist,用来存放我们的应用程序捆绑包的 Info.plist。并添加以下内容:(LSUIElement可将我们的应用程序标识为代理,这样它就不会在 Dock 或任务切换器上显示图标)。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>LSUIElement</key>
<true/>
</dict>
</plist>
现在为我们的 XPC 服务创建 Plist 文件。创建名称为 XPCInfo.plist 的文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.rderik.ServiceProviderXPC</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>XPCService</key>
<dict>
<key>ServiceType</key>
<string>Application</string>
</dict>
</dict>
</plist>
这些是 XPC 服务捆绑包 Info.plist 文件的必填字段。CFBundleIndentifier 被用作服务名称。稍后,我们将在定位服务时使用该名称。
文件结构应如下所示
.
├── Package.swift
├── README.md
├── Sources
│ └── XPCSquirrel
│ ├── AppDelegate.swift
│ └── main.swift
├── SupportFiles
│ ├── MainInfo.plist
│ └── XPCInfo.plist
└── Tests
├── LinuxMain.swift
└── XPCSquirrelTests
├── XCTestManifests.swift
└── XPCSquirrelTests.swift
好了,现在可以专注于 XPC 服务的代码了。
最后,让我们使用 XPC。
我们将在 Package.swift 中添加一个新目标,它将代表我们的 XPC 服务代码。
编辑 Package.swift 并添加新目标:
// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "XPCSquirrel",
dependencies: [
],
targets: [
.target(
name: "XPCSquirrel",
dependencies: []),
.testTarget(
name: "XPCSquirrelTests",
dependencies: ["XPCSquirrel"]),
.target(
name: "ServiceProvider",
dependencies: []),
]
)
现在,让我们在 Sources 中创建一个新目录,其中将包含我们的 XPC 代码。
$ mkdir Sources/ServiceProvider
在 Sources/ServiceProvider 中新建一个名为 main.swift 的文件。这将是我们 XPC 服务捆绑包的入口点。我们的 main 将侦听新连接并处理它们的请求。
工作流程如下
(1) 我们创建一个监听器,(2) 设置其委托对象。委托对象负责接受和设置新的传入连接。一旦监听器有了委托对象,我们就会调用 resume 来指示监听器 (3) 开始 "监听 "连接。
这就是我们的 main.swift 看起来的样子:
import Foundation
let listener = NSXPCListener.service()
let delegate = ServiceDelegate()
listener.delegate = delegate;
listener.resume()
RunLoop.main.run()
我们还没有创建 ServiceDelegate,所以现在就创建吧。我们的委托应实现 NSXPCListenerDelegate 协议,该协议要求实现设置传入连接的 listener 函数。监听器 "具有以下签名:
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool
返回的布尔值表示连接是否被接受。我们可以进行任何验证,然后决定是否接受连接。
好了,让我们开始处理委托。在 Sources/ServiceProvider 中创建一个名为 ServiceDelegate.swift 的新文件,并添加以下内容:
import Foundation
class ServiceDelegate : NSObject, NSXPCListenerDelegate {
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
newConnection.exportedInterface = NSXPCInterface(with: ServiceProviderXPCProtocol.self)
let exportedObject = ServiceProviderXPC()
newConnection.exportedObject = exportedObject
newConnection.resume()
return true
}
}
首先,我们设置由 ServiceProviderXPCProtocol代表的导出接口(接下来我们将创建该协议)。该协议定义了服务的接口。它定义了我们提供的函数及其签名。我们现在处于连接的 "服务器 "端,但在 "客户端 "也需要有定义接口的协议。
设置 "exportedInterface "后,我们将创建一个实现协议的对象。该对象将在请求服务函数时被调用。我们将连接的 exportedObject 属性设置为实现协议的对象。
创建新连接时,它将以停止状态开始执行。我们使用 resume 来启动它。
如果一切顺利,我们将返回 true 表示连接已被接受和配置。
正如我们之前所决定的,我们的 XPC 服务将提供一个服务来获取我们的公共 IP,并使用我们的 IP 执行我们作为参数获取的闭包(withReply)。
创建文件 ServiceProviderXPCProtocol.swift 并添加以下内容:
import Foundation
@objc(ServiceProviderXPCProtocol) protocol ServiceProviderXPCProtocol {
func getPublicIp(withReply reply: @escaping (String) -> Void)
}
现在,让我们创建一个新文件来实现协议。在 Sources/ServiceProvider/ 中创建文件 ServiceProviderXPC.swift。并添加以下内容:
import Foundation
@objc class ServiceProviderXPC: NSObject, ServiceProviderXPCProtocol{
func getPublicIp(withReply reply: @escaping (String) -> Void) {
let pmset = Process()
let pipe = Pipe()
if #available(OSX 13, *) {
pmset.executableURL = URL(fileURLWithPath: "/usr/bin/env")
} else {
pmset.launchPath = "/usr/bin/env"
}
// We are going to use `dig` to obtain our public IP using
// Cisco's opendns.com domain:
// dig +short myip.opendns.com @resolver1.opendns.com
pmset.arguments = ["dig", "+shor", "myip.opendns.com", "@resolver1.opendns.com"]
pmset.standardOutput = pipe
do {
if #available(OSX 13, *) {
do {
try pmset.run()
} catch {
reply("")
}
} else {
pmset.launch()
}
pmset.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let output = String(data: data, encoding: String.Encoding.utf8) {
reply(output.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
}
}
该函数使用 dig 命令获取我们的公共 IP。大部分代码都与运行 dig 命令有关,但与 XPC 相关的代码只是将通过 dig 获得的 ip 发送到 reply 闭包。
好了,我们的 XPC 服务端已经完成。让我们在主程序中访问我们的 XPC 服务。
使用我们的 XPC 服务
苹果公司做得非常出色,使 XPC 服务的使用非常简单。工作流程如下:
(1) 我们创建一个与要使用的服务的连接。服务名称(XPC 服务的 "Info.plist "中的 "CFBundleIdentifier",还记得吗?) (2)设置远程对象的接口,这里我们需要知道描述接口的协议,也就是我们在 XPC 服务捆绑包上创建服务时使用的协议。(3) 获取实现接口的对象实例(使用 "远程对象代理")。(4) 最后,使用服务。请记住,调用始终是异步的,这意味着如果我们要使用用户界面,就需要通过 main 队列发送,这样就不会阻塞主线程。
好了,让我们看看我们的实现。我们将使用 AppDelegate.swift 文件。让我们在 statusBarMenu 中添加一个新选项,它允许我们调用方法来显示我们的公共 IP。
statusBarMenu.addItem(
withTitle: "Get public ip",
action: #selector(AppDelegate.xpcCall),
keyEquivalent: "")
我们需要定义函数 xpcCall。让我们开始吧
@objc func xpcCall() {
let connection = NSXPCConnection(serviceName: "com.rderik.ServiceProviderXPC")
connection.remoteObjectInterface = NSXPCInterface(with: ServiceProviderXPCProtocol.self)
connection.resume()
let service = connection.remoteObjectProxyWithErrorHandler { error in
print("Received error:", error)
} as? ServiceProviderXPCProtocol
service!.getPublicIp() { (texto) in
DispatchQueue.main.async {
self.statusBarItem.button?.title = "\(texto)"
}
}
}
正如您所看到的,我们使用 ServiceProviderXPProtocol 定义了 remoteObjectInterface ,但我们还没有为主应用程序定义它。你可以从 Sources/ServiceProvider/ 目录中复制并粘贴到 Sources/XPCSquirrel 目录中。
我们正在使用 connection.remoteObjectProxyWithErrorHandler,它允许我们传递一个闭包来处理任何错误,并获取一个实现了 ServiceProviderXPCProtocol 的对象实例。
然后,我们使用内联闭包语法糖传递闭包,调用函数 getPublicIp(withReply:) 。在闭包中,我们将 statusBarItem.button?.title 设置为从服务中获取的 ip。
很酷吧?
接下来,你将看到完整的 AppDelegate.swift 文件。我还做了一些额外的改动。例如,显示计数器的选项。这样,在检查完公共 ip 后,我们就可以继续把 App 用作计数器了。
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
var statusBarItem: NSStatusItem!
var counter: Int = 0
func applicationDidFinishLaunching(_ aNotification: Notification) {
let statusBar = NSStatusBar.system
statusBarItem = statusBar.statusItem(
withLength: NSStatusItem.variableLength)
statusBarItem.button?.title = "🌰 \(counter)"
let statusBarMenu = NSMenu(title: "Counter Bar Menu")
statusBarItem.menu = statusBarMenu
statusBarMenu.addItem(
withTitle: "Increase",
action: #selector(AppDelegate.increaseCount),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Decrease",
action: #selector(AppDelegate.decreaseCount),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Get public ip",
action: #selector(AppDelegate.xpcCall),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Show Counter",
action: #selector(AppDelegate.showCounter),
keyEquivalent: "")
statusBarMenu.addItem(
withTitle: "Quit",
action: #selector(AppDelegate.quit),
keyEquivalent: "")
}
@objc func showCounter() {
statusBarItem.button?.title = "🌰 \(counter)"
}
@objc func increaseCount() {
counter += 1
showCounter()
}
@objc func decreaseCount() {
counter -= 1
showCounter()
}
@objc func xpcCall() {
let connection = NSXPCConnection(serviceName: "com.rderik.ServiceProviderXPC")
connection.remoteObjectInterface = NSXPCInterface(with: ServiceProviderXPCProtocol.self)
connection.resume()
let service = connection.remoteObjectProxyWithErrorHandler { error in
print("Received error:", error)
} as? ServiceProviderXPCProtocol
service!.getPublicIp() { (texto) in
DispatchQueue.main.async {
self.statusBarItem.button?.title = "\(texto)"
}
}
}
@objc func quit() {
NSApplication.shared.terminate(self)
}
}
现在我们有了所有代码,但仍需将所有内容整合在一起。我们需要构建应用程序,并将 XPCService 添加到主应用程序捆绑包的适当位置。应用程序捆绑包可以根据需要拥有任意多个 XPC 服务捆绑包,它们位于 Contents 中的 XPCServices 文件夹内。
我将使用 make 自动完成这一过程,这样我们就不必全部手工完成了。请看下面的 Makefile:
SUPPORTFILES=./SupportFiles/
PLATFORM=x86_64-apple-macosx
BUILD_DIRECTORY = ./.build/${PLATFORM}/debug
APP_DIRECTORY=./XPCSquirrel.app
CFBUNDLEEXECUTABLE=XPCSquirrel
XPCEXECUTABLE=ServiceProvider
install: build copySupportFiles
build:
swift build --product ${CFBUNDLEEXECUTABLE}
swift build --product ${XPCEXECUTABLE}
copySupportFiles:
mkdir -p ${APP_DIRECTORY}/Contents/MacOS/
mkdir -p ${APP_DIRECTORY}/Contents/XPCServices/${XPCEXECUTABLE}.xpc/Contents/MacOS/
cp ${SUPPORTFILES}/MainInfo.plist ${APP_DIRECTORY}/Contents/Info.plist
cp ${SUPPORTFILES}/XPCInfo.plist ${APP_DIRECTORY}/Contents/XPCServices/${XPCEXECUTABLE}.xpc/Contents/Info.plist
cp ${BUILD_DIRECTORY}/${CFBUNDLEEXECUTABLE} ${APP_DIRECTORY}/Contents/MacOS/
cp ${BUILD_DIRECTORY}/${XPCEXECUTABLE} ${APP_DIRECTORY}/Contents/XPCServices/${XPCEXECUTABLE}.xpc/Contents/MacOS/
run: build
open -a ${APP_DIRECTORY}
clean:
rm -rf .build
rm -rf ${APP_DIRECTORY}
.PHONY: run build copySupportFiles clean
如你所见,我们创建了所有应用程序捆绑包结构并创建了 XPC 捆绑包。让我们运行 make 命令并生成 XPCSquirrel.app.
$ make
# this will create the `XPCSquirrel.app` Application bundle
# you can use `make clean` to remove it
这将生成具有以下结构的 XPCSquirrel.app 捆绑程序:
XPCSquirrel.app
└── Contents
├── Info.plist
├── MacOS
│ └── XPCSquirrel
└── XPCServices
└── ServiceProvider.xpc
└── Contents
├── Info.plist
└── MacOS
└── ServiceProvider
6 directories, 4 files
现在,你可以在 Finder 上双击应用程序或在 shell 上使用 open 命令来运行它:
如果在任务栏中看不到应用程序,请切换到 Finder,它可能被当前应用程序中的某些菜单项遮住了。现在你可以点击 "获取公共 IP",然后就能在任务栏中看到你的 IP 了。
恭喜你 你刚刚在应用程序中使用了 XPC 服务。
最后的想法
除了在应用程序中创建 XPC 服务包,XPC 作为进程间通信还有更多用途。在我看来,最有用的用途之一就是创建一个 "启动代理"(LaunchAgent),它可以提供一些服务,让许多应用程序都能连接到它并获取信息。
例如,一个进行语法和拼写检查的应用程序(也许是 Grammarly?) 该应用程序可以公开一个 XPC 服务,你可以发送一个文本块,它会返回一个包含更正的对象。
注意在使用 XPC 服务时小写的 "s",我们说的不是一个捆绑包,而是一个可以从其他应用程序访问的服务。
好了,本周就到这里。如果你喜欢这篇文章和你对 XPC 的其他使用,请告诉我。
你可以在 GitHub 仓库 查看完整代码。
相关主题/注意事项
- Apple's documentation on Creating XPC Services, also, Daemons and Services Programming Guide
- objc.io 发表的一篇关于 XPC 的精彩文章
- 另一篇关于使用 Xcode 创建 XPC 服务捆绑包的好文章
- 除了苹果公司的 API 文档外,我没有找到太多有关 XPC 的资源。为了让它正常工作,我做了很多尝试和错误。以下是 Apple - XPC 文档