环境 / Environment
- Glint
0.1.27-beta.1 (build 202607300827), app.glint.Glint
- macOS 27.0 (26A5388g), Apple M1 Max
- 复现于 upstream
main@d297ef4,代码与 release 一致 / reproduced against upstream main@d297ef4
现象 / Symptom
两天内随机闪退两次,无操作规律,秒级重开后正常。系统没有留下 .ips(本机
DiagnosticReports 被清理过),证据取自 unified log。
Two random crashes in two days. No user-visible trigger, no dialog — the whole
window (and every terminal in it) just disappears.
| pid |
崩溃时刻 |
存活时长 |
| 4958 |
2026-08-04 23:57:06 |
~13 h |
| 6555 |
2026-08-05 19:23:32 |
~19 h |
两次的异常类型、抛出点、调用栈逐帧相同。
崩溃栈 / Stack
崩溃前 15–25 ms,两次都先出现:
ViewBridge: assertion failed: '<private>' in -[NSRemoteView containingWindowWillOrderOnScreen:] on line 4221
ViewBridge: Error Domain=com.apple.ViewBridge.error Code=8 ... NSViewBridgeErrorException
SafariPlatformSupport: View service did terminate with error: <private>
然后:
FAULT: NSInternalInconsistencyException [com.apple.hiservices:HIExceptions]
0 CoreFoundation __exceptionPreprocess
1 libobjc.A.dylib objc_exception_throw
3 ViewBridge -[NSRemoteView containingWindowWillOrderOnScreen:]
4 CoreFoundation __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__
8 Foundation -[NSNotificationCenter postNotificationName:object:userInfo:]
9 AppKit -[NSWindow _doWindowWillBeVisibleAsSheet:]
10 AppKit -[NSWindow _reallyDoOrderWindowAboveOrBelow:]
13 AppKit _NSWindowWalkOrderingGroupInternal
14 AppKit -[NSWindow _rebuildOrderingGroup:]
16 AppKit -[NSWindow addChildWindow:ordered:]
17 AppKit -[NSPopover showRelativeToRect:ofView:preferredEdge:]
18 SwiftUI PopoverBridge.updatePopover(...)
21 SwiftUI PopoverBridge.preferencesDidChange(_:)
22 SwiftUI NSHostingView.preferencesDidChange()
23 SwiftUICore ViewGraph.updateOutputs(at:)
27 SwiftUI NSHostingView.layout()
37 AppKit ___NSViewLayout_block_invoke
56 QuartzCore CA::Transaction::run_commit_handlers
60 AppKit stepTransactionFlush
74 AppKit -[NSApplication run]
注意 Glint 自己的代码不在抛出栈上。
机制 / Mechanism
- macOS 会把跨进程的
NSRemoteView 挂进主窗口的 ordering group —— 这些 UI 应用
从未主动请求:文本输入光标 HUD(TextInputUI.xpc.CursorUIViewService)和
数据检测器(SafariPlatformSupport.Helper)。两次崩溃前的日志里都能看到它们
随着终端输入被反复 activate。
- 系统回收其中一个 view service 后(
View service did terminate,ViewBridge
Code=8),主进程侧的 NSRemoteView 仍留在窗口层级里。
- 下一个 child window 上屏时,
-[NSWindow addChildWindow:ordered:] →
_rebuildOrderingGroup: 向 ordering group 内所有 view 广播
containingWindowWillOrderOnScreen:。
- 那个孤儿 remote view 断言失败并抛
NSInternalInconsistencyException。
- 这个异常一路无人捕获,进程被终止。
所以触发条件是「系统回收 view service 的时机」+「此时恰好弹出一个 child window」,
应用侧完全不可控,也解释了为什么看起来随机。
触发面 / Trigger surface
main@d297ef4 上共 6 处 .popover:
Glint/Agent/AgentPaneSummary.swift:258 — hover 350 ms 后自动弹出,无需点击
Glint/Chrome/ContentView.swift:1213 / 1521 / 1703
Glint/Chrome/SettingsView.swift:2138 / 2363
异常 reason 被 <private> 屏蔽,无法确定具体是哪一处;AgentPaneSummary 因为
hover 自动弹出,触发频率最高。不过任何 sheet / menu 也走同一条
addChildWindow: 广播路径,所以这不是某个 popover 的问题。
已验证无效的方案 / Measured and rejected
为省掉重复劳动,这两条我做了对照实验,都无效:
- 覆盖
-[NSApplication reportException:] —— 这条路径上它从未被调用。
最小复现(SwiftUI app,从 NSView.layout() raise)两种配置都是 exit 133
(SIGTRAP),覆盖的方法一次都没进去。异常是 uncaught,不是 reported。
NSSetUncaughtExceptionHandler —— handler 返回后进程照样终止,救不回来。
顺带一个可能对其他改动有用的发现:SwiftUI app 无法通过 Info.plist 的
NSPrincipalClass 替换 NSApplication 子类 —— SwiftUI.runApp 在调用
NSApplicationMain 之前就已经取到了 NSApplication.shared,principal class 那时
被忽略。要换子类只能在 App.init() 里抢先 MyApplication.shared。
可行的修复 / What does work
把 -[NSWindow addChildWindow:ordered:] 包进 ObjC 的 @try/@catch:它是包住
整个广播的最窄的一帧,而 popover / sheet / menu 全都从这里过。
对照实验(纯 ObjC,通知 observer 在 child window 上屏时 raise,栈结构与生产崩溃
逐帧对应):
|
结果 |
| 无 guard |
*** Terminating app due to uncaught exception, exit 134 |
| 有 guard |
捕获,进程存活,exit 0 |
判定收窄到只吞这一类:异常名为 NSInternalInconsistencyException 且调用栈里
有 ViewBridge / NSRemoteView 帧;其余一律重抛,应用自身的 bug 仍然照常崩。
吞掉时打一条 fault 日志,不静默。代价上限是一次 popover 没弹出来。
我在 fork 上已经实现并验证:312 个测试通过,Release 构建通过,新增测试覆盖判定的
命中/不命中两个方向并断言 guard 确实安装在真实启动路径上。
如果你觉得这个方向可以,我可以直接提一个 PR。 也完全理解如果你更倾向别的做法
(比如不用 swizzle)—— 上面的机制分析和实验数据应该够定位了。
环境 / Environment
0.1.27-beta.1(build202607300827),app.glint.Glintmain@d297ef4,代码与 release 一致 / reproduced against upstreammain@d297ef4现象 / Symptom
两天内随机闪退两次,无操作规律,秒级重开后正常。系统没有留下
.ips(本机DiagnosticReports被清理过),证据取自 unified log。Two random crashes in two days. No user-visible trigger, no dialog — the whole
window (and every terminal in it) just disappears.
两次的异常类型、抛出点、调用栈逐帧相同。
崩溃栈 / Stack
崩溃前 15–25 ms,两次都先出现:
然后:
注意 Glint 自己的代码不在抛出栈上。
机制 / Mechanism
NSRemoteView挂进主窗口的 ordering group —— 这些 UI 应用从未主动请求:文本输入光标 HUD(
TextInputUI.xpc.CursorUIViewService)和数据检测器(
SafariPlatformSupport.Helper)。两次崩溃前的日志里都能看到它们随着终端输入被反复 activate。
View service did terminate,ViewBridgeCode=8),主进程侧的
NSRemoteView仍留在窗口层级里。-[NSWindow addChildWindow:ordered:]→_rebuildOrderingGroup:向 ordering group 内所有 view 广播containingWindowWillOrderOnScreen:。NSInternalInconsistencyException。所以触发条件是「系统回收 view service 的时机」+「此时恰好弹出一个 child window」,
应用侧完全不可控,也解释了为什么看起来随机。
触发面 / Trigger surface
main@d297ef4上共 6 处.popover:Glint/Agent/AgentPaneSummary.swift:258— hover 350 ms 后自动弹出,无需点击Glint/Chrome/ContentView.swift:1213 / 1521 / 1703Glint/Chrome/SettingsView.swift:2138 / 2363异常 reason 被
<private>屏蔽,无法确定具体是哪一处;AgentPaneSummary因为hover 自动弹出,触发频率最高。不过任何 sheet / menu 也走同一条
addChildWindow:广播路径,所以这不是某个 popover 的问题。已验证无效的方案 / Measured and rejected
为省掉重复劳动,这两条我做了对照实验,都无效:
-[NSApplication reportException:]—— 这条路径上它从未被调用。最小复现(SwiftUI app,从
NSView.layout()raise)两种配置都是 exit 133(SIGTRAP),覆盖的方法一次都没进去。异常是 uncaught,不是 reported。
NSSetUncaughtExceptionHandler—— handler 返回后进程照样终止,救不回来。顺带一个可能对其他改动有用的发现:SwiftUI app 无法通过
Info.plist的NSPrincipalClass替换NSApplication子类 ——SwiftUI.runApp在调用NSApplicationMain之前就已经取到了NSApplication.shared,principal class 那时被忽略。要换子类只能在
App.init()里抢先MyApplication.shared。可行的修复 / What does work
把
-[NSWindow addChildWindow:ordered:]包进 ObjC 的@try/@catch:它是包住整个广播的最窄的一帧,而 popover / sheet / menu 全都从这里过。
对照实验(纯 ObjC,通知 observer 在 child window 上屏时 raise,栈结构与生产崩溃
逐帧对应):
*** Terminating app due to uncaught exception, exit 134判定收窄到只吞这一类:异常名为
NSInternalInconsistencyException且调用栈里有
ViewBridge/NSRemoteView帧;其余一律重抛,应用自身的 bug 仍然照常崩。吞掉时打一条 fault 日志,不静默。代价上限是一次 popover 没弹出来。
我在 fork 上已经实现并验证:312 个测试通过,Release 构建通过,新增测试覆盖判定的
命中/不命中两个方向并断言 guard 确实安装在真实启动路径上。
如果你觉得这个方向可以,我可以直接提一个 PR。 也完全理解如果你更倾向别的做法
(比如不用 swizzle)—— 上面的机制分析和实验数据应该够定位了。