深入分析:macOS 上 Arc 浏览器与 AeroSpace 窗口管理器的焦点冲突与工作区闪回 Bug

作为一名 Linux power user,我曾长期依赖基于 Chromium 的 Arc 浏览器作为主力,主要因为其强大的插件生态和 macOS 上的原生体验,特别是 Workspaces 功能对管理大量标签页(常占用数十 GB 内存)极为有效。然而,近期我遇到了一个与 AeroSpace 平铺窗口管理器结合使用时的严重 Bug,迫使我最终迁移到了基于 Firefox 的 Zen 浏览器。本文旨在深入剖析这个 Bug 的技术细节和根本原因。

这个 Bug 并非简单的兼容性问题,而是涉及到 Arc 浏览器(及其底层 Chromium/Electron 框架)的特定行为、macOS 窗口管理机制(WindowServer)以及 AeroSpace 独特的虚拟工作区实现方式之间的复杂交互。

问题详述:令人抓狂的工作区闪回

我使用 AeroSpace,一个受 i3 启发的 macOS 平铺窗口管理器。它以其高效、无动画的工作区切换、纯文本配置和无需禁用系统完整性保护(SIP)的特性吸引了我。

然而,当 Arc 浏览器窗口处于活动状态时,尝试使用 AeroSpace 的快捷键从包含 Arc 的工作区(记为 W)切换到另一个工作区(记为 T)时,会出现一种极其干扰工作流的现象:

  1. 屏幕短暂地切换到目标工作区 T。
  2. 几乎在瞬间(几毫秒内),屏幕又被强制拉回到原始工作区 W。
  3. 缓慢地、一次次地按下切换快捷键,只会稳定地复现这种「闪回」行为。
  4. 只有以极快的速度连续敲击快捷键,才有可能「冲破」这种回弹,成功切换到目标工作区 T。

这种行为不仅令人沮丧,而且严重破坏了平铺窗口管理器旨在提升的流畅工作体验。

技术探索:深入调试之旅

我的调试过程遵循了由表及里、逐步排除的思路:

初步怀疑:浮动窗口干扰?

AeroSpace 文档提到,它使用启发式策略识别浮动窗口(如偏好设置、对话框)以避免平铺它们,但某些应用可能需要手动在配置文件(aerospace.toml)中通过 [[on-window-detected]] 规则显式配置 run = 'layout floating'。我首先怀疑是否存在某个不可见或微小的浮动窗口干扰了 AeroSpace 的窗口状态判断。然而,即使在关闭所有可疑的窗口元素(如 Input Source Pro, Kitty 的搜索 overlay 等)并确保没有其他可能产生浮动窗口的应用运行时,问题依旧稳定复现。

社区线索:从 AeroSpace 到 yabai

接下来,我转向开源社区寻求相关信息。在 AeroSpace 的 GitHub Issues 中,我找到了 #289 Chrome window: Looping/Bouncing Between Workspaces。该 issue 描述的现象与我的高度相似,但报告者使用的是 Chrome。这立刻将我的注意力引向了底层技术栈——Chromium。Arc 正是基于 Chromium 构建的。

为了扩大搜索范围并获取更多信息(考虑到 yabai 用户基数更大且可能遇到更广泛的系统交互问题,尽管它需要禁用 SIP),我查阅了 yabai 的 GitHub Issues。果然,我发现了两个高度相关的 issue:

这些 issue 中的讨论提到了几个关键点:

  • 尝试调整 macOS 系统设置,如「System Preferences」>「Desktop & Dock」>「Mission Control」下的「Displays have separate Spaces」(显示器具有单独的空间)和「When switching to an application, switch to a Space with open windows for the application」(切换到应用程序时,切换到包含该应用程序打开窗口的空间)。
  • 部分用户反馈禁用后者有效,但也有用户表示无效,并直接指出「Arc retaining focus when switching spaces is a problem with the app itself」。

关键差异:AeroSpace 的虚拟工作区实现

对于 AeroSpace 用户来说,上述 macOS 系统设置的调整几乎是无效的,这源于 AeroSpace 精巧(也是其核心 features 之一)的虚拟工作区仿真机制:

  • 不依赖原生 Spaces:为了避免原生 macOS Spaces 的性能问题、动画延迟以及与某些 API 的不兼容性(尤其是在不禁用 SIP 的情况下),AeroSpace 默认不使用 macOS 的多 Space 功能。
  • 基于 Accessibility API:AeroSpace 将所有受其管理的窗口都保留在用户的第一个真实 macOS Desktop 上。它利用 macOS 的 Accessibility API 来模拟工作区的切换。
  • 「显-隐」法:当用户切换工作区时,AeroSpace 并不移动窗口。它通过 Accessibility API 调用,将不属于目标工作区的窗口隐藏起来,并将属于目标工作区的窗口显示出来。这里的「隐藏」并非简单的最小化,而是通过设置窗口的屏幕位置将其移出可见区域(例如,设置到屏幕左上角之外的一个坐标点,内部实现大致是调用 MacWindow 对象的 set(attribute: kAXPositionAttribute, value: point))。代码库中将其封装为 hideInCornerunhideFromCorner 这两个函数。
  • 单一私有 API 调用:AeroSpace 致力于避免使用「黑魔法」(私有 API、代码注入等)。目前仅使用了一个私有 API _AXUIElementGetWindow,目的是从 Accessibility 对象(AXUIElement)可靠地获取其对应的窗口 ID (CGWindowID)。除此之外,皆依赖公开的 Accessibility API。

Good monitor arrangement. Every monitor has free space in either of the bottom corners

图示:在 AeroSpace 中良好的显示器布局,所有窗口都隐藏在角落里。

由于所有窗口物理上都位于 Desktop 1,macOS 的「When switching to an application, switch to a Space with open windows for the application」设置自然对 AeroSpace 的工作区切换行为没有任何实质影响。这也意味着,yabai issue 中提到的系统设置调整对 AeroSpace 无效。

锁定真凶:Arc 与 makeKeyAndOrderFront:

yabai issue 中「问题在于 Arc 应用自身」的评论以及我在 Reddit 上找到的帖子 Cmd+Tab switching spaces on macos? 提供了决定性的证据。该 Reddit 帖子描述了即使用户关闭了「切换应用时切换 Space」设置,使用系统原生的 Cmd+Tab 切换到 Arc 时,系统仍然会强制切换到包含 Arc 窗口的 Space。

我亲自验证了这一点:在 Desktop 2 使用 Cmd+Tab 尝试仅将焦点赋予位于 Desktop 1 的 Arc 窗口(预期行为是焦点转移,但我应留在 Desktop 2),结果却被系统强制拉回了 Desktop 1。

这清晰地表明,Arc 自身存在一种行为,会主动且强制性地将用户带到其窗口所在的上下文。经过进一步深入挖掘,我了解到这种行为与 macOS AppKit 框架中的一个核心方法有关:-[NSWindow makeKeyAndOrderFront:]

makeKeyAndOrderFront: 详解:

此方法通常由应用程序在需要确保其窗口成为用户交互的主要目标时调用。它执行两个关键操作:

  1. makeKeyWindow: 将该窗口设置为主窗口(Key Window)。Key Window 是当前接收键盘事件的窗口。macOS 的 WindowServer(负责管理窗口显示和事件路由的核心系统进程)有一个基本规则:Key Window 所在的 Space(或在 AeroSpace 的场景下,所在的 Desktop)必须是当前活动的、显示在前台的 Space/Desktop。如果 Key Window 位于非活动的 Space/Desktop,WindowServer 会自动执行切换,将该 Space/Desktop 带到前台。
  2. orderFront:: 将该窗口置于其层级(level)中的最前方,并确保它是可见的(如果之前被隐藏)。这会覆盖掉 AeroSpace 通过 Accessibility API 施加的「隐藏」(移出屏幕)效果。

Arc (及 Chromium/Electron) 的频繁调用:

问题在于,Arc(以及其基础 Chromium 框架,乃至许多 Electron 应用)在多种后台或半后台情况下会频繁调用 makeKeyAndOrderFront:

  • 标签页或内容更新:切换标签页、页面加载完成、动态内容更新(如标题变化)。
  • 浏览器 UI 交互:显示「Little Arc」侧边小窗、下载完成通知弹出、地址栏/命令栏(Command Bar)动画或状态更新、侧边栏(Sidekick)交互。
  • 扩展程序活动:某些浏览器扩展的回调函数可能会触发此调用。

相比纯粹的 Chrome 或 Edge,Arc 具有更丰富的、动态更新的前端 UI 元素,这导致它调用 makeKeyAndOrderFront: 的频率可能更高。

Bug 根源:API 冲突与机制干扰

现在,我们可以完整地描绘出工作区闪回 Bug 的发生机制:

  1. 用户切换工作区 (W → T):用户按下 AeroSpace 快捷键。AeroSpace 使用 Accessibility API 将工作区 W 中的 Arc 窗口「隐藏」(移出屏幕),并将工作区 T 的窗口「显示」。此时,所有窗口仍在物理上的 Desktop 1。
  2. Arc 后台调用 API:几乎同时,Arc 可能因为上述某种原因(例如,一个标签页标题更新或命令栏状态改变)在后台线程调用了 -[NSWindow makeKeyAndOrderFront:]
  3. Arc 强制可见并成为 Key WindoworderFront: 部分使得被 AeroSpace 「隐藏」的 Arc 窗口强制恢复可见。makeKeyWindow 部分使其成为当前的 Key Window。
  4. WindowServer 介入:macOS WindowServer 检测到当前的 Key Window (Arc) 位于 Desktop 1,并且该窗口刚刚变为可见/活动。由于 Key Window 必须位于前台 Desktop,WindowServer 强制将用户的视图切换回 Desktop 1(即使 AeroSpace 正试图让用户停留在逻辑上的工作区 T)。
  5. AeroSpace 响应变化:AeroSpace 通过监听窗口可见性和焦点变化来维护状态。它检测到 Arc 窗口意外地重新出现并获得了焦点,这在它的状态机看来,似乎是用户意图返回到 Arc 所属的工作区 W。因此,AeroSpace 执行了一次反向的「切换」操作,试图将状态同步回工作区 W,最终将用户锁定在了 W。

这个 AeroSpace 隐藏 → Arc 调用 API → WindowServer 强制前台 → AeroSpace 误判并反向切换 的循环,就是导致工作区连续闪回的根本原因。这不是 AeroSpace 的 Bug,也不是严格意义上 Arc 的 Bug,而是两者机制在特定场景下的不幸冲突,其核心驱动力是 Arc(或底层框架)过于频繁和主动地使用 makeKeyAndOrderFront: 来管理自身窗口状态。

Arc/AeroSpace 工作区闪回循环

普遍性问题:需要强调的是,这个问题并非 Arc 独有。任何基于 Chromium(Chrome, Edge, Brave 等)或 Electron(VS Code, Slack, Obsidian, Discord 等)的应用,只要其内部逻辑在后台触发了 makeKeyAndOrderFront:,理论上都可能与 AeroSpace 的虚拟工作区机制产生类似冲突。我确实也找到过零星报告 VS Code 出现类似问题的 issue。Arc 只是由于其特定的 UI 实现导致触发频率更高,使得问题暴露得更明显。

解决方案:釜底抽薪——更换浏览器引擎

理解了问题的技术根源后,最直接有效的解决方案就是避免使用会触发这种冲突行为的应用程序。既然问题与 Chromium/Electron 框架密切相关,我决定尝试基于不同引擎的浏览器。

我选择了 Zen 浏览器。Zen 是一个开源项目,旨在提供类似 Arc 的用户体验(如 Workspace 功能),但它基于 Firefox 的 Gecko 引擎构建。

迁移到 Zen 之后,工作区切换闪回的问题彻底消失了。AeroSpace 的工作区切换恢复了其应有的丝滑流畅。这有力地证明了问题确实源于 Arc/Chromium 的特定行为,而 Firefox/Gecko 在这方面表现不同,不会在后台如此激进地争夺焦点。

另外,我已经向 Aerospace 的开发者提交了 discussion Arc (Chromium) window keeps yanking focus back to its workspace – causes rapid 「flash-back」 loop when using AeroSpace virtual workspaces,希望他们能注意到这个问题。

结论与反思

macOS 上 Arc 浏览器与 AeroSpace 窗口管理器结合使用时的工作区切换闪回 Bug,是一个典型的由应用程序特定行为(频繁调用 makeKeyAndOrderFront:)、操作系统窗口管理机制(WindowServer 对 Key Window 的处理)以及第三方窗口管理器独特实现(AeroSpace 的虚拟工作区仿真)三者交互产生的复杂问题。

通过细致的调试、对相关工具和系统机制的深入理解,最终定位到问题的根源在于 Arc/Chromium 对 makeKeyAndOrderFront: 的调用模式与 AeroSpace 的工作方式不兼容。解决此问题的最有效方法是更换使用了不同底层引擎(Firefox/Gecko)的浏览器,如 Zen。