Hotdry.

Article

用 NSStatusItem/NSDockTile API 实现 macOS 任务栏替换的工程实践

基于 AppKit NSStatusItem 与 NSDockTile API,探讨 macOS 任务栏替换的技术路径、权限模型与工程化参数。

2026-04-13systems

macOS 系统的 Dock 栏以应用为核心组织逻辑,一个应用无论打开多少个窗口,在 Dock 中仅占用一个图标位置。这种设计在单显示器、单桌面场景下足够直观,但当用户同时使用多个显示器、多个虚拟桌面(Spaces)时,快速定位特定窗口的效率急剧下降。boringbar 作为近期出现在 Hacker News 的开源项目,提供了任务栏风格的 Dock 替换方案:以桌面为维度展示窗口_chip_,支持即时预览、单击桌面切换、固定应用到栏中等特性。本文从 AppKit API 层面剖析此类实现的技术路径、工程参数与监控要点。

NSStatusItem:菜单栏应用的根基

NSStatusItem 是 AppKit 框架中用于在系统菜单栏(左上角)创建常驻项的核心类。与 Dock 不同,NSStatusItem 位于系统菜单栏区域,不受 Spaces 切换影响,能够提供持久的存在感。其 API 设计相对简洁:通过 NSStatusBar.system.statusItem(withLength:) 获取实例后,可将任意 NSView 赋值给 button?.view 属性,从而在菜单栏实现自定义 UI。

boringbar 的核心 UI 即基于 NSStatusItem 构建。每个打开的窗口对应一个芯片(chip),芯片上显示应用图标、窗口标题或应用名称、以及未读通知徽章。用户点击芯片时,应用通过 NSWorkspace.shared.runningApplicationsCGWindowListCopyWindowInfo 查询窗口句柄,随后调用 NSWorkspace.shared.activateAXUIElement 模拟用户激活目标窗口。NSStatusItem 的长度参数通常设为 .variableLength 以适应动态内容宽度,但若追求一致的视觉密度,可使用 .squareStatusItemLength 配合固定尺寸的 icon。

在实现层面,NSStatusItem 的自定义视图需要处理鼠标事件与键盘焦点。当用户悬停芯片时,boringbar 通过 CGWindowListCreateImage 定时抓取窗口缩略图并显示在弹出层中。这一步依赖 Screen Recording 系统权限,否则 CGWindowListCreateImage 返回 nil,缩略图功能将失效。权限检测可在应用启动时通过 CGPreflightScreenCaptureAccess() 完成,若返回 false 则提示用户手动授权。

NSDockTile:历史路径与当代限制

NSDockTile API 允许应用自定义自身在 Dock 中的展示方式,包括设置徽章、替换图标、注入自定义视图、以及通过 dockMenu() 返回右键菜单项。早期的 macOS 曾支持 NSDockTilePlugIn(Dock 瓦片插件),使应用即使在未运行时也能更新 Dock 中的展示内容,这一机制被部分第三方应用用于实现类似任务栏的动态信息显示。

然而,自 macOS 10.14 起,系统对 NSDockTile 插件的加载机制进行了显著限制。插件必须位于应用包 Contents/PlugIns 目录下,且沙箱策略对插件可执行的代码做了严格约束。对于希望完全接管 Dock 行为(如显示所有窗口列表、实现桌面切换)的开发者而言,NSDockTile 并非可行路径 —— 它只能修改 “当前应用自身” 的 Dock 展示,无法替代系统 Dock 的整体功能。

在工程实践中,NSDockTile 的典型用途限于两类场景:其一,应用作为后台进程时通过徽章(badge)向用户传递状态信息,如邮件客户端的未读计数;其二,通过 dockMenu() 为用户提供快捷操作入口。boringbar 等任务栏替换工具本质上是一个独立的菜单栏应用,并不依赖 NSDockTile 实现其核心功能。

权限模型与实现参数

构建类似 boringbar 的任务栏替换工具,需要在权限与性能两个维度进行工程化设计。

权限层面:应用需要请求 Accessibility 权限以调用 AXUIElement API 观察与控制窗口。通过 AXIsProcessTrusted() 可检测当前授权状态,若返回 false 则引导用户前往系统偏好设置的安全与隐私 > 隐私 > 辅助功能中开启。Screen Recording 权限仅在需要生成窗口缩略图时必须,可通过检测 CGPreflightScreenCaptureAccess() 确认。两项权限均为运行时请求,用户拒绝后应用功能将部分降级。

窗口监控参数:使用 CGWindowListCopyWindowInfo 枚举窗口时,建议将选项设为 [.optionOnScreenOnly, .excludeDesktopElements],以排除桌面元素与不可见窗口的干扰。窗口列表的刷新频率不宜超过 500 毫秒一次,过于频繁的系统调用会导致 CPU 占用显著上升。实践中可维护一个本地缓存的 windowID -> WindowInfo 映射表,仅在 kCGWindowListDidChangeNotification 通知触发时进行增量更新。

多显示器与 Spaces 支持:当系统开启 “显示器具有独立 Spaces” 时,每个显示器拥有独立的桌面编号。应用需通过 CGWindowListCopyWindowInfo 中的 kCGWindowOwnerPID 关联应用进程,并通过 NSWorkspace.shared.notificationCenter 监听 NSWorkspace.activeSpaceDidChangeNotification 以实时更新当前显示器的窗口列表。若用户的 “显示器具有独立 Spaces” 设为关闭,所有显示器共享同一 Spaces 逻辑,此时窗口列表应返回全局桌面视图。

UI 刷新与动画:boringbar 中的 attention pulse(注意力脉冲)通过 NSAnimationContext 控制,脉冲时长建议设为 600 毫秒、透明度从 1.0 衰减至 0.4,以在人眼可感知范围内提供足够提醒而不造成视觉干扰。窗口切换动画若使用 NSView.animate,持续时间推荐 200–300 毫秒,配合 .curveEaseInOut 缓动函数。

监控与回滚策略

生产环境中的任务栏替换应用应建立以下监控指标:Accessibility 权限状态、Screen Recording 权限状态、窗口枚举耗时(目标 < 50ms)、内存占用(目标 < 80MB)、CPU 空闲占用(目标 < 2%)。当权限被用户手动撤销时,应用应在菜单栏显示警示图标并弹出通知,引导用户重新授权。窗口枚举耗时若超过阈值,应触发日志记录与性能剖析,以排查是否有异常窗口(如全屏视频播放)导致枚举延迟。

回滚策略方面,当应用检测到 CGWindowListCopyWindowInfo 连续三次返回空列表但系统 Dock 仍有窗口时,应判定为权限被撤销或系统 API 异常,此时切换至简化模式 —— 仅显示固定应用列表而不尝试枚举窗口,确保用户仍可通过应用启动器访问常用程序。

小结

macOS 任务栏替换的技术路径聚焦于 NSStatusItem API 而非 NSDockTile—— 前者提供了在菜单栏构建自定义 UI 的完整能力,后者仅适用于单一应用自身的 Dock 展示增强。工程实现的关键在于权限模型的正确处理、窗口列表的高效枚举、以及多显示器与 Spaces 场景下的逻辑适配。boringbar 作为该细分方向的实践案例,验证了以桌面为维度的窗口组织方式能够显著提升多任务工作流的效率,其权限请求、刷新频率、动画参数等工程细节可为同类项目提供直接参考。

资料来源:boringbar 官方产品页面(https://boringbar.app)与 Apple 开发者论坛关于 Dock 菜单的讨论(https://developer.apple.com/forums/thread/762250)。

systems