扩展虚拟化 App 的功能
Expand the capabilities of your Virtualization app
2026年6月10日
一句话判断
✅ DiskImageKit 和 USB 直通的加入,让第三方虚拟机 App 终于能摆脱“玩具”标签,直接硬刚 Parallels Desktop 的核心体验。
这场 Session 讲了什么
以前用 Virtualization.framework(虚拟化框架)搞虚拟机,最大的痛点是“半吊子”。装完系统得手动点 Setup Assistant(设置助理),USB 设备挂载全靠玄学,磁盘镜像随便一搞就几十上百 G,网络配置也只能用基本的 NAT 或 Bridge。这使得 UTM 或 Tart 等开源方案在体验上始终差商业软件一口气。
现在 Apple 把脏活累活都接管了。VZMacGuestProvisioningOptions 让你用代码直接跳过开机向导;Accessory Access(配件访问)框架把 USB 直通做得像 iOS 外接设备一样优雅;全新的 DiskImageKit 引入了类似 Docker 的分层镜像概念;甚至开放了 Custom Virtio(自定义半虚拟化设备)让你自己写虚拟硬件驱动。
这对需要在 CI/CD 里跑 macOS 虚拟机的团队是直接的生产力提升,对独立虚拟机开发者更是史诗级利好。你不再需要为了一个 USB 挂载功能去逆向底层 IOKit,Apple 直接把饭喂到了嘴边。
值得深挖的点
DiskImageKit:虚拟机镜像的“容器化”革命
以前每个 VM 都是一个巨大的 .qcow2 或 .raw 文件,克隆一个 50G 的 macOS VM 就要实打实占用 50G 物理空间。Apple 这次推出的 DiskImageKit 框架,直接抄了 Docker 镜像分层的作业。
它支持 Base(基础层)、Cache(缓存层)和 Overlay(覆盖层)。Base 层只读,存放干净的 macOS 系统;Cache 层存临时数据;Overlay 层存用户的增量修改。当你需要启动 10 个相同的测试节点时,只需要 1 个 50G 的 Base 镜像,加上 10 个几 MB 的 Overlay 文件。
这种设计极大地节省了磁盘空间,让“秒级克隆 VM”成为可能。不过实战中要当心文件锁和并发写入问题。如果 Base 层在 VM 运行期间被意外修改或替换,所有依赖它的 VM 都会直接 kernel panic(内核崩溃)。在做自动化清理脚本时,务必确保 Base 镜像处于严格的只读保护状态。
Accessory Access:把 USB 直通做成“系统级权限弹窗”
在虚拟化里直通 USB 设备一直是个大坑。以前要么靠底层强行抢占设备(容易导致宿主机死机),要么体验极差,拔插设备时 VM 经常卡死。
Apple 这次没有直接给 API 让开发者强行霸占 USB,而是引入了 Accessory Access 框架。它的 trade-off(权衡)很明显:牺牲了一点全自动化的便利性,换取了绝对的安全性和热插拔稳定性。开发者通过 AAUSBAccessoryMatchingCriteria 注册感兴趣的设备,当设备插入时,macOS 菜单栏会弹出一个类似 AirDrop 的系统级菜单,必须由用户手动点击“连接到 App”。
这种设计对 C 端用户非常友好,杜绝了流氓软件偷偷读取 USB 键盘或 U 盾的风险。但对于想做全自动化测试的团队来说,这个“用户显式授权”可能会成为阻碍。你可能需要借助 UI 自动化脚本去点击那个菜单栏按钮,或者在特定测试环境下寻找 bypass(绕过)方法。
代码片段
1. 自动化 macOS 首次启动配置
场景:CI/CD 流水线中,新建的 macOS VM 需要自动创建用户并开启 SSH,实现 zero-touch(零接触)初始化。
import Virtualization
// 创建配置选项,设置用户名、密码和自动登录
let provisioningOptions = VZMacGuestProvisioningOptions()
provisioningOptions.fullName = "CI Runner"
provisioningOptions.username = "runner"
provisioningOptions.password = "securePassword123" // 实战中请从 Keychain 读取
provisioningOptions.logsInAutomatically = true
provisioningOptions.enablesRemoteLogin = true // 开启 SSH
let startOptions = VZMacOSVirtualMachineStartOptions()
try startOptions.setGuestProvisioning(provisioningOptions)
// 首次启动时,设置助理会自动消费这些参数
try await virtualMachine.start(options: startOptions)
坑:这些参数只在 VM 处于“未设置”状态(首次启动)时有效。如果 VM 已经走过 Setup Assistant,后续启动传入这些选项会被直接忽略。
2. 使用 DiskImageKit 构建分层镜像
场景:基于一个干净的 macOS Base 镜像,快速启动多个带有独立增量数据的 VM 实例。
import DiskImageKit
import Virtualization
// 以只读模式打开基础镜像
let baseImage = try DiskImage(opening: .open(url: baseLayerURL, mode: .readOnly))
// 追加一个缓存层,用于存放临时或频繁变动的数据
let cacheImage = try baseImage.appending(.asifLayer(url: cacheLayerURL, type: .cache))
// 打开并追加覆盖层,存放用户的持久化增量修改
let overlayImage = try DiskImage(opening: .open(url: overlayLayerURL))
let stackedImage = try cacheImage.appending(overlayImage)
// 将堆叠后的镜像挂载到虚拟存储设备
let storageDeviceAttachment = try VZDiskImageStorageDeviceAttachment(diskImage: stackedImage)
let storageDeviceConfiguration = VZVirtioBlockDeviceConfiguration(attachment: storageDeviceAttachment)
virtualMachineConfiguration.storageDevices = [storageDeviceConfiguration]
坑:层级越深,I/O 性能损耗越大。建议 Base + Overlay 两层即可,Cache 层仅在明确需要隔离临时文件(如浏览器缓存)时使用。
3. 响应 USB 配件热插拔
场景:用户通过系统菜单将 U 盘授权给 VM 后,App 捕获事件并将其直通到虚拟机的 USB 控制器。
import AccessoryAccess
import Virtualization
class AccessoryListener: NSObject, AAUSBAccessoryListener {
// 当用户在菜单栏点击“连接到 App”时触发
func usbAccessoryDidConnect(_ usbAccessory: AAUSBAccessory) {
// 必须在 VM 专属的 queue 中修改硬件配置
virtualMachine.queue.async {
do {
let configuration = VZUSBPassthroughDeviceConfiguration(device: usbAccessory)
let device = try VZUSBPassthroughDevice(configuration: configuration)
// 将设备动态挂载到 VM 的 USB 控制器上
self.virtualMachine.usbControllers.first?.attach(device: device) { error in
if let error = error { print("挂载失败: \(error)") }
}
} catch {
print("配置设备失败: \(error)")
}
}
}
}
坑:别忘了在 Xcode 的 Target Capabilities 里添加 “Claim USB Accessory” 权限,否则 registerListener 会直接报错。
最佳实践
已有项目的迁移策略:如果你在做类似 UTM 的虚拟机 App,立刻把存储层重构为 DiskImageKit。提供“秒级克隆”和“共享基础镜像”功能,这会成为你最大的卖点。同时,把以前那些 hack 出来的 USB 挂载代码全删了,换上 Accessory Access,能帮你省去 80% 的崩溃客诉。
新项目的采用建议:做 CI/CD 虚拟机池的团队,用 VZMacGuestProvisioningOptions 配合 Tart 等工具,把节点初始化时间从几分钟压缩到几十秒。网络方面,直接用 vmnet 框架配置自定义 DHCP 和端口转发,别再在宿主机上折腾 dnsmasq 或 pf 脚本了。
实战中容易踩的坑:处理 Accessory Access 时,用户随时可能通过菜单栏断开设备。你的 App 必须妥善实现 usbAccessoryDidDisconnect 回调,及时清理 VM 内部的挂载点,否则 Guest OS(客户机操作系统)的文件系统可能会因为底层设备突然消失而损坏。
还有什么值得关注
- Custom Virtio 开放了底层协议:通过
VZCustomVirtioDevice,你可以自己定义 PCI 设备 ID 和 Virtio Queue,这意味着你可以为特定的硬件加速卡编写半虚拟化驱动,实现 Host 和 Guest 之间的极速通信。 - vmnet 支持高级网络拓扑:现在可以直接在代码里配置 TCP/UDP 端口转发规则,把宿主机的特定端口直接映射到 VM 内部,做本地微服务联调极其方便。
- 密码安全警告:Apple 在 Session 中特意强调了 Provisioning API 的密码处理,千万别把明文密码硬编码在源码里,老老实实走 Keychain(钥匙串)或环境变量。