Hotdry.

Article

boringBar 的架构抉择:为何选择 NSStatusItem 而非 NSDockTile

解析 boringBar 作为任务栏风格 Dock 替代方案的技术选型,深度对比 NSStatusItem 与 NSDockTile 的工程实现差异及架构考量。

2026-04-13systems

在 macOS 生态中,实现一个类 Dock 功能的应用通常有两条技术路径:基于 NSStatusItem 的菜单栏应用,或基于 NSDockTile 的 Dock 磁贴插件。boringBar 作为一款任务栏风格的 Dock 替代应用,明确选择了前者。本文将从产品架构视角,解析这一技术选型的深层逻辑,并对比两种实现路径的工程差异。

技术路径的本质差异

理解 boringBar 的架构选择,首先需要厘清 NSStatusItem 与 NSDockTile 在系统层面的本质差异。NSStatusItem 是 macOS 菜单栏应用的核心组件,本质上是嵌入系统菜单栏区域的控件,由你的应用进程直接托管和渲染。应用退出后,NSStatusItem 随之消失,状态更新完全依赖应用进程的生命周期。相比之下,NSDockTile 采用插件化架构,通过 NSDockTilePlugIn 接口将自定义逻辑注入 SystemUIServer 进程运行。这意味着 Dock 磁贴可以在宿主应用未启动时仍保持动态更新能力 —— 这一特性在某些场景下是优势,但也带来了更复杂的进程间通信与生命周期管理问题。

boringBar 团队选择 NSStatusItem 作为核心架构,核心原因在于其功能需求更适合同进程内的实时状态管理。boringBar 需要持续监听当前 Space 的窗口变化、响应用户在不同桌面间的切换操作、实时渲染窗口缩略图并维护通知角标状态。这些高频状态更新场景下,NSStatusItem 的同进程架构避免了插件模式下的进程通信开销,同时也降低了系统稳定性风险。NSDockTile 插件运行在 SystemUIServer 进程中,一旦插件代码出现异常可能导致整个系统 UI 崩溃,这是追求稳定性的消费级应用难以接受的风险。

权限模型与功能边界的权衡

boringBar 的功能实现高度依赖两个 macOS 系统权限:Accessibility 和 Screen Recording。从产品角度审视这一权限依赖,可以看出其架构设计与功能边界之间的紧密关联。Accessibility 权限用于观察和交互窗口、桌面及应用程序,这使得 boringBar 能够获取当前 Space 下所有窗口的标题、位置和层级关系,并实现窗口切换、焦点转移等交互操作。Screen Recording 权限则专门用于获取窗口缩略图预览 —— 值得注意的是,boringBar 团队在产品文档中明确说明,该权限仅用于缩略图获取,不会用于任何形式的屏幕录制或数据外传。

这一权限模型揭示了一个关键架构约束:boringBar 必须作为一个独立的前台或后台应用运行,而非 Dock 插件。因为 NSDockTile 插件在获取系统窗口信息和缩略图方面存在显著限制 —— 插件运行在 SystemUIServer 的受限上下文中,无法直接调用 Accessibility API 来遍历窗口层级,也难以访问每个应用的窗口渲染上下文。boringBar 需要在自有进程中持有这些权限,才能持续轮询窗口列表、捕获缩略图并更新 UI 状态。若采用 NSDockTile 插件架构,权限继承和传递将变得极其复杂,且插件进程的可执行能力受到系统严格限制。

窗口组织模型的设计哲学

boringBar 与原生 macOS Dock 在窗口组织理念上存在根本性差异,这直接影响了其架构设计。苹果的 Dock 以应用程序为核心组织单元 —— 同一应用的多个窗口被折叠为单一图标,用户需要通过右键菜单或缩略图展开才能看到具体窗口。当用户打开数十个浏览器窗口或文档窗口时 Dock 会变得臃肿且难以辨识。boringBar 则采用了任务栏风格的以窗口为中心的组织模型:每个窗口对应一个独立的「芯片」(chip),芯片上显示应用图标、窗口标题摘要以及通知角标。用户可以一目了然地看到当前 Space 下所有打开的窗口,并直接点击切换。

这种设计在 NSStatusItem 架构下实现更为自然。NSStatusItem 支持自定义 NSView 或 SwiftUI 视图,开发者可以在菜单栏区域渲染复杂的自定义 UI。boringBar 将菜单栏区域扩展为一个水平滚动的窗口芯片条带,每个芯片对应一个窗口实例,芯片的点击区域直接绑定到窗口切换操作。这种交互模式与 Windows 任务栏或 Linux 面板高度一致,为从其他操作系统迁移到 macOS 的用户提供了熟悉的使用体验。

多显示器与 Space 策略的工程实现

boringBar 在多显示器环境下的处理方式体现了其工程实现的精细度。产品支持两种部署模式:在系统设置「Displays have separate Spaces」启用时,每个显示器拥有独立的 Space 集合;在该选项关闭时,所有显示器共享同一组 Space。boringBar 需要同时支持这两种配置,并在菜单栏上呈现对应的窗口集合。

这一需求在 NSStatusItem 架构下的实现方式是:在每个菜单栏项(NSStatusItem)上注册 NSDockTile 不具备的空间感知回调。boringBar 监听 macOS 的 spaceDidChange 通知,当用户切换 Space 或显示器时,重新查询当前 Space 下的窗口列表并刷新 UI。由于应用进程持续运行,这种状态同步可以做到几乎无延迟。NSDockTile 插件在这一点上面临更大挑战:插件需要在 SystemUIServer 中自行实现空间变化的监听逻辑,且缺乏直接查询当前 Space 窗口的高层 API,只能通过底层 CGR 接口遍历窗口,复杂度显著提升。

桌面切换功能是 boringBar 的核心交互之一。用户可以通过滚动菜单栏区域在不同 Space 之间切换,也可以在桌面切换器视图中点击目标 Space 直接跳转。这一功能依赖 Accessibility 接口中的 AXUIElement API,通过设置 AXFocusedApplication 和 AXFrontmost 属性来实现 Space 切换。NSStatusItem 的点击事件处理在应用主进程中执行,可以直接调用这些 Accessibility 接口,无需额外的进程间调度。

缩略图预览的技术链路

窗口缩略图预览是 boringBar 的差异化功能点,也是其架构设计的技术难点。用户将鼠标悬停在窗口芯片上时,系统会在芯片下方弹出该窗口的实时缩略图,帮助用户在切换前确认目标窗口的内容。这一功能的技术链路如下:首先通过 Screen Recording 权限调用 CGWindowListCreateImage,传入目标窗口的 windowID 和可见区域矩形,截取窗口的当前渲染画面;然后将捕获的图像缓存并渲染到弹出视图中。

在 NSStatusItem 架构下,这一流程嵌入应用的主事件循环中,缩略图请求与 UI 渲染在同一个进程中完成,延迟可控制在数十毫秒级别。若采用 NSDockTile 插件架构,缩略图捕获需要在 SystemUIServer 进程内完成,且需要处理与宿主应用之间的图像数据传输。由于插件的运行环境限制,CGWindowListCreateImage 在某些 macOS 版本中可能返回空图像或被系统拒绝调用。boringBar 选择 NSStatusItem 架构,很大程度上规避了这些兼容性风险。

给开发者的架构启示

从 boringBar 的案例可以提炼出几个关键的架构决策原则。首先,当应用需要持续监听系统状态变化、频繁更新 UI 且依赖完整系统权限时,NSStatusItem 架构优于 NSDockTile 插件。后者更适合那些需要在 Dock 磁贴上显示静态或简单动态内容(如天气、日历日期),且不依赖复杂权限的轻量级场景。其次,产品功能边界往往由系统权限模型决定 ——boringBar 选择的功能特性(窗口列表、缩略图、空间切换)决定了它必须作为一个全功能应用运行,而非 Dock 插件。第三,NSStatusItem 的自定义视图能力被低估了 —— 它不仅可以渲染简单的图标按钮,还可以承载水平滚动的复杂 UI 布局,这为类任务栏应用提供了足够的渲染灵活性。

对于计划构建类似产品的团队,核心建议是:在项目早期明确功能需求与系统权限的依赖关系,据此选择技术路径。若目标是高度交互的窗口管理工具,NSStatusItem 是更稳健的选择;若只需在 Dock 磁贴上展示简单状态信息,则 NSDockTile 插件的轻量级优势更为突出。两种架构并非优劣之分,而是针对不同产品需求的适配选择。


参考资料

systems