Mac App Store 不允许窗口管理器存在,我还是把它上架了
一位 Apple 开发者技术支持(DTS)工程师告诉我,我的这个应用不可能做出来:“无法在沙盒应用中使用 Accessibility API……您唯一的出路是直接分发您的应用。”1 那个应用是一款窗口平铺工具,而 Accessibility API 正是每一款窗口管理器赖以存在的根基,Mac App Store 上的每一个应用都运行在禁用它的沙盒之内。可上周,941 Tiles 还是在 Mac App Store 上线了:在菜单栏点击一次,某个应用散落各处的窗口就会全部对齐成一个均匀的网格。沙盒规则一次也没有被弯折。关键在于注意到一件事:macOS 里本来就有一个得到 Apple 完全认可、拥有特权的窗口搬运者,而且它接受 JSON。
TL;DR
- App Sandbox 一律阻断跨应用的 Accessibility 调用,没有任何 entitlement 能解锁它,而 Mac App Store 上的每一个应用都必须沙盒化。自 2012 年 6 月起,窗口管理器这一品类对新来者实际上已经关闭;仍留在商店里的平铺工具早于这项强制要求,因此可以永远不沙盒化地运行。134
- 那扇获准的门:沙盒应用可以通过 Apple events 驱动 Apple 的 Shortcuts 引擎,而 Shortcuts 自带 Find Windows / Move Window / Resize Window 动作,能以 Apple 自己的权限移动其他应用的窗口。25 我的应用负责计算每个窗口应该去哪里;真正搬运的是一个配套的快捷指令。这是委托,不是许可。
- 让它落地的工程细节:用程序把配套的
.shortcut生成为WFWorkflowActions的二进制 plist,一份以窗口 z 序为键的 JSON 帧负载约定,针对幽灵窗口和索引越界的防护栏,以及一套双引擎代码库——Developer ID 版本走直接的 Accessibility,商店版本走 Shortcuts 委托。 - 在那台曾让五个商店版本折戟的机器上,第一次成功运行:四个散落的终端窗口在 0.6 秒内落到精确的四分区框内。
- 功劳归于应得之人:这一机制已由 snApp(开源,2023 年)和 Align(2024 年)在实践中验证。把“不可能”变成实现方案的,正是读了 snApp 的源代码。2
那堵墙
941 Tiles 的第一个版本是一款典型的菜单栏平铺工具。macOS 有一套 Accessibility 系统,在获得用户许可后,可以让一个应用枚举并移动另一个应用的窗口(对窗口位置和尺寸属性调用 AXUIElementSetAttributeValue)。建立在这之上的应用,在数周的开发里运行得毫无瑕疵。我把整个 App Store Connect 的商品页都备齐了:17 种语言的元数据、截图、定价、21 个已上传的版本。
然后我把一个 TestFlight 版本装到了第二台 Mac 上。Accessibility 权限已授予,应用也报告访问已开启,可它能读到的窗口是 0 个。
这不是 bug。Mac App Store 上的每一个应用都运行在 Apple 的 App Sandbox 里,这是一条隔离规则:应用只能触碰属于自己的东西,正是为了让它无法窥探或干扰你 Mac 上的其他部分。4 伸手去够另一个应用的窗口,恰恰是沙盒存在的目的所要阻止的。开发版本不沙盒化运行,所以数周里一切正常;商店版本执行的,是我一直忽略的那条规则。没有权限对话框、没有 entitlement、没有任何设置能解锁它。当我直接去问 Apple 时,DTS 的答复就是本文开头那段:到商店之外去分发。1
一个自 2012 年起关闭的品类
这堵墙之所以耐人寻味,在于站在墙那一侧的是谁。Magnet、Moom、BetterSnapTool:今天你仍能在 Mac App Store 上买到的窗口工具。它们的商品页早于 2012 年 6 月的沙盒强制要求,因此依据一条不会转移给任何其他人的祖父条款,可以不沙盒化地运行。2012 年之后诞生的每一款窗口管理器(Rectangle、Swish、Loop、BetterTouchTool、Amethyst)都在商店之外分发。Apple 也在反方向上执行过这条线:在商店待了将近 13 年的 PopClip,在审核告知其开发者不得在沙盒化的更新中加入新的 Accessibility 功能后,于 2024 年 3 月离开。3
我想把自己的看法说明白:沙盒规则是合理的。一个能悄无声息地读取并移动你屏幕上每一个窗口的应用,就是一个能监视你的应用,而“相信我”不是安全模型。商店的承诺是:你在那里装的任何东西都做不到这件事。不公平之处很窄、很具体:在位者保住了一种新来者无论获得多少用户同意都无法赢得的能力。这个品类没有被禁止,它被化石化了。
转折
DTS 的判决有一个漏洞:商店里本来就有反例。Align(2024 年)和 snApp(2023 年)都在平铺窗口,都活在 Mac App Store 上,也都诞生于 2012 年之后很久。snApp 是开源的,所以这套机制并不是秘密,它是一个代码仓库。2
沙盒应用从不触碰另一个应用的窗口。它通过两项声明的 entitlement 去驱动 Shortcuts,也就是 Apple 自己的自动化应用:
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.temporary-exception.apple-events</key>
<array>
<string>com.apple.shortcuts.events</string>
</array>
自 macOS 12 起,Shortcuts 提供了名为 Find Windows、Move Window 和 Resize Window 的动作。5 Shortcuts 被允许移动窗口,因为它是 Apple 的。于是应用计算出每个窗口该去哪里,把这份方案交给用户安装的配套快捷指令,由 Apple 自己的引擎完成搬运。沙盒从未被违反,因为那个沙盒进程从未执行特权动作。这是委托,不是许可。
这个形状里有某种近乎建筑学的意味:安全边界仍然停在 Apple 划下的位置,而能力则借由系统上唯一正当持有它的那个进程,越过了边界。
落地过程
知道机制和把它做出来上架,是两个不同的星期。让它真正达到生产水准的部分包括:
用程序生成快捷指令。 用户不该手工拼装一个 12 步的工作流。配套的 .shortcut 文件是一份 WFWorkflowActions 的二进制 plist,由应用在构建时生成,并用 shortcuts sign -m anyone 签名,因此安装只需在 Shortcuts 应用里点一下。运行时的约定是一份 JSON 负载,经由 ScriptingBridge 的 shortcut.run(withInput:) 传入:
{"app": "Ghostty", "frames": [{"x": 0, "y": 38, "w": 1280, "h": 771}, ...]}
窗口的身份就是 z 序。 快捷指令的 Find Windows 动作按从前到后为窗口编号,CGWindowList 枚举的顺序也一样。因此负载中的第 i 个槽位承载的就是窗口 i 的边框;再按位置排名分配格子,网格就复现了“窗口停在它原本所在的附近”。没有窗口 ID 跨越边界,因为边界另一侧根本不存在窗口 ID。
防护栏。 一个以 Count 为界的 Items in Range 钳制,可以防止过期的负载索引到超出当前窗口列表的位置。一道最小尺寸过滤(120×80)排除了 CGWindowList 会报告、而旧的 Accessibility 角色检查曾经隐藏掉的幽灵停放面板。还有一个让我意外的事实:列出窗口根本不需要任何权限,CGWindowList 在沙盒里是合法的。一直以来被禁止的只有移动。
一套代码库,两个引擎。 应用在启动时通过 APP_SANDBOX_CONTAINER_ID 环境变量检测沙盒。商店版本经由 Shortcuts 平铺;Developer ID 版本和开发版本走直接的 Accessibility 路径。相同的 UI、相同的算法,不同的搬运者。
没有通知的监视模式。 可选启用的自动重排功能通常会订阅 Accessibility 的窗口通知,而这同样被沙盒拿掉了。它以窗口服务器轮询的形式活了下来:做廉价的 CGWindowList 差分,一有变化就重排。
老实说说代价
用户要付一笔一次性的设置税:安装配套的快捷指令(点一下),并同意一次自动化授权对话框(就是标准的“941 Tiles 想要控制 Shortcuts”提示,通过 InfoPlist.xcstrings 本地化为全部 17 种语言)。此后的体验与直接调用 API 的平铺工具完全一致:点击网格图标,点击某个应用,窗口就位。Shortcuts 的运行不可见,用时远不到一秒。
我不会假装这笔税等于零;它确实比 Magnet 多出一屏。它同时也是活在沙盒之内所需付出的全部代价——相比之下,另一条路是请用户为一个他们还没有理由信任的公证下载去绕开 Gatekeeper。
回报
还是那台曾让五个商店版本折戟的 Intel Mac:安装、设置快捷指令、授权,然后四个散落的 Ghostty 窗口在第一次运行时于 0.6 秒内落到精确的四分区框内。接着是国际化工作(String Catalogs、CLDR 复数规则,包括阿拉伯语全部六个类别)、本地化的商品页,以及提交。经过一次审核拒绝和约 30 分钟的复审,941 Tiles 已上线:一次性 4.99 美元,macOS 15 及以上,17 种语言,零数据收集,无网络访问。
可以推广的部分
这个模式比窗口平铺本身更大。当一个平台把某项能力关在边界之后,答案很少是去和边界硬碰;而是清点有哪些第一方进程已经持有这项能力,并且对外提供了获准的接口。在 macOS 上,这份清单越来越指向同一个地方:Shortcuts。它已经悄悄成为系统的特权自动化中枢,而且按设计就可以从沙盒内部触达。我的 Shortcuts 指南深入讲了这一层,941 Tiles 案例研究则配着架构图走了一遍同样的故事。
沙盒尽了它的职责。应用还是上架了。这两句话都是真的,而它们之间的那扇门,是 Apple 自己的。
常见问题
沙盒化的 Mac 应用可以使用 Accessibility API 吗?
不可以。App Sandbox 一律阻断跨应用的 Accessibility 调用,没有任何 entitlement、权限对话框或设置能解锁它。Apple 自家 DTS 给我的答复是:“无法在沙盒应用中使用 Accessibility API……您唯一的出路是直接分发您的应用。”开发版本不沙盒化运行,这就是为什么一款平铺工具可以正常工作好几周,然后在商店版本里读到 0 个窗口。14
窗口管理器要怎样才能在 Mac App Store 上架?
靠委托那个特权动作。沙盒应用可以通过 Apple events 驱动 Apple 的 Shortcuts 引擎,而 Shortcuts 自 macOS 12 起就提供了 Find Windows、Move Window 和 Resize Window 动作。我的应用计算出每个窗口该去哪里,把这份方案交给用户安装的配套快捷指令,由它以 Apple 自己的权限完成搬运。沙盒从未被违反,因为那个沙盒进程从未执行特权动作。25
既然窗口管理器不可能上架,为什么 Magnet 和 Moom 在商店里?
它们的商品页早于 2012 年 6 月的沙盒强制要求,因此依据一条不会转移给任何其他人的祖父条款,可以不沙盒化地运行。2012 年之后诞生的每一款窗口管理器,包括 Rectangle、Swish、Loop、BetterTouchTool 和 Amethyst,都在商店之外分发。PopClip 在审核告知其开发者不得在沙盒化的更新中加入新的 Accessibility 功能后,于 2024 年 3 月离开商店。这个品类没有被禁止,它被化石化了。34
Shortcuts 委托让用户付出什么代价?
一笔一次性的设置税:点击一次安装配套的快捷指令,然后同意一次自动化授权对话框。此后的体验与直接调用 API 的平铺工具完全一致:点击网格图标,点击某个应用,窗口就位,而 Shortcuts 的运行不可见,用时远不到一秒。它比 Magnet 多出一屏,而这就是活在沙盒之内所需付出的全部代价。
一套代码库如何同时支持沙盒版本和直接版本?
应用在启动时通过 APP_SANDBOX_CONTAINER_ID 环境变量检测沙盒。商店版本经由 Shortcuts 平铺;Developer ID 版本和开发版本走直接的 Accessibility 路径。相同的 UI、相同的算法,不同的搬运者。监视模式同样会适配:沙盒拿掉了 Accessibility 的窗口通知,所以可选启用的自动重排以廉价的 CGWindowList 轮询存活下来,做差分,一有变化就重排。
参考资料
-
Apple Developer Forums,话题 805556(2025 年 10 月),Apple DTS 的答复:“无法在沙盒应用中使用 Accessibility API……您唯一的出路是使用 Developer ID 签名直接分发您的应用。” ↩↩↩↩
-
Om Chachad,GitHub 上的 snApp(2023 年),这款开源的 Mac App Store 窗口平铺工具,其源代码记录了 Shortcuts 委托机制:
com.apple.security.automation.apple-events脚本目标 entitlement,加上一个用 Apple 的窗口动作搭建的配套快捷指令。 ↩↩↩↩ -
Michael Tsai,《PopClip Leaving the Mac App Store》(2024 年 3 月 21 日),讲的是 Apple 审核要求一款在商店待了将近 13 年的应用,从沙盒化的更新中移除新的 Accessibility 功能。 ↩↩↩
-
Apple,App Sandbox 文档,自 2012 年 6 月起 Mac App Store 上所有应用都被要求采用的隔离模型。 ↩↩↩↩
-
Apple,Mac 版 Shortcuts 使用手册,其中记录了自 macOS 12 起可用的 Find Windows、Move Window 和 Resize Window 动作。 ↩↩↩