
CodexBar 的 WebKit 实践可见购买窗口、离屏抓取与 WebKitTeardown 安全销毁指南【免费下载链接】CodexBarShow usage stats for OpenAI Codex and Claude Code, without having to login.项目地址: https://gitcode.com/GitHub_Trending/co/CodexBarCodexBar 作为 macOS 菜单栏应用在“不要求用户登录也能看到用量”的产品目标下把 WebKit 用在了两个位置可见的积分购买窗口以及用于 OpenAI 仪表盘抓取的离屏 WebView。本文以仓库内的 docs/webkit.md 为骨架结合 WebKitTeardown.swift、OpenAICreditsPurchaseWindowController.swift 和 OpenAIDashboardWebViewCache.swift 的源码实现讲清楚这两类 WebView 宿主的生命周期管理、Intelx86_64平台上的 teardown 崩溃规避策略以及 Cookie 存储的选择原则读完你可以复现“安全创建—使用—延迟释放”一套完整的 WebKit 窗口销毁方案。WebKit 在 CodexBar 中的两个使用位置docs/webkit.md 开篇即明确CodexBar 使用 WebKit 的地方只有两处可见的购买窗口例如 credits purchase 积分购买窗口对应实现是 OpenAICreditsPurchaseWindowController.swift窗口标题为 “Buy Credits”离屏 WebViewoffscreen WebViews用于 OpenAI 仪表盘ChatGPT Codex 分析页的抓取对应实现集中在 OpenAIDashboardWebViewCache.swift 中的OpenAIDashboardWebViewCache与私有宿主类OffscreenWebViewHost。两类宿主的共同点是流程结束后都会“消失”购买窗口被关闭、离屏宿主被隐藏/关闭而 WebKit 的回调线程与主线程对象在窗口关闭瞬间并不同步结束。文档因此给出核心约束任何WKWebViewNSWindow的销毁或隐藏都应通过WebKitTeardown这个集中式辅助工具完成不要直接close()。可见购买窗口窗口构建与关闭时清理OpenAICreditsPurchaseWindowController 的buildWindow()展示了与 WebKit 配合的标准窗口配置其中两个细节直接服务于后续的 teardown 策略window.isReleasedWhenClosed falseL440这正是 docs/webkit.md Notes 部分要求的“当 teardown 是延迟执行时保持isReleasedWhenClosed false”。原因是WebKitTeardown在窗口关闭后还会异步操作该窗口先orderOut、延迟释放若窗口在close()时被 ARC 释放后续回调会触碰已释放对象。config.websiteDataStore使用按账号隔离的存储L416-L418OpenAIDashboardWebsiteDataStore.store(forAccountEmail:scope:)让购买窗口的 Cookie 与当前登录账号绑定避免多账号场景互相污染。真正的清理发生在windowWillClose中L538-L548先清空控制器自身持有的webView/window引用然后把对象所有权移交给辅助工具func windowWillClose(_ notification: Notification) { guard let window self.window else { return } let webView self.webView self.pendingAutoStart false self.webView nil self.window nil self.logger.info(Buy credits window closing) WebKitTeardown.scheduleCleanup(owner: window, window: window, webView: webView) }注意这里owner传的是window本身——WebKitTeardown会在内部用ObjectIdentifier建立保留表接管这个对象的“延迟释放权”。离屏抓取宿主让 WebKit 认为页面“可见”的障眼法离屏抓取比可见窗口多一个难题目标页面ChatGPT 用量页是流式 SSR 客户端 hydration 的 SPA。OffscreenWebViewHost 的构造代码里有一段很长的注释点破了原理WebKit 会对它认为“不可见”的 WKWebView 激进度节流定时器与 requestAnimationFrame一旦 RAF 被节流仪表盘永远无法完成 hydrationdocument.body.innerText会一直很小。为此宿主窗口做了三重“伪可见”处理窗口位置几乎完全移出屏幕只留 1×1 px 交集。OpenAIDashboardFetcher.offscreenHostWindowFrame 把窗口原点放在visibleFrame.maxX - 1, visibleFrame.maxY - 1窗口尺寸为min(1200, 屏宽) × min(1600, 屏高)这样既不会在桌面上“幽灵渲染”出仪表盘 UI又能让 WebKit 判定其处于可见区域alphaValue必须大于 0。offscreenHostAlphaValue() 固定返回0.001源码注释明确写道alpha 为 0 时曾观察到页面长时间停留在“只有 HTML 外壳”的状态hydration 被卡住数分钟无边框、透明、忽略鼠标事件styleMask: [.borderless]、backgroundColor .clear、ignoresMouseEvents true并设置window.isReleasedWhenClosed falseL664同样是为了配合延迟 teardown。此外该缓存还有能耗考量hide()会把alphaValue置 0 并orderOut让 WebKit 识别页面为“非活动”从而进入节流/挂起调度以降低 CPU 占用L687-L693而 WebView 本身只保留足够应付“立即重试/菜单重开”的时长默认idleTimeout: 60秒后由空闲修剪逻辑驱逐因为长期保留隐藏的 ChatGPT 标签页在部分机器上能耗可观L131-L133 的注释。宿主销毁时走的是带闭包的 teardown 调用close()L695-L710func close() { guard !self.isClosed else { return } self.isClosed true ... WebKitTeardown.scheduleCleanup( owner: self, window: self.window, webView: self.webView, closeWindow: { [window] in window.orderOut(nil) window.close() }) }closeWindow闭包的存在说明WebKitTeardown允许调用方自定义“真正关闭”的动作离屏宿主需要orderOutclose两步而不是强制统一window.close()。WebKitTeardown集中式延迟销毁与 Intel 崩溃规避WebKitTeardown 是一个MainActor的公共枚举文档中“centralizes Intel-friendly cleanup”的说法在源码里对应三层机制。平台分叉的常量表常量含义x86_64其他架构Apple SiliconretainAfterCleanup清理后是否改为仅隐藏、延迟释放truefalsecleanupDelay停止加载到执行清理的等待0.25s0.25sreleasePollInterval释放条件轮询间隔0.2s0.2sreleaseMinimumDelay无活动后最短保留时间2.0s2.0sreleaseMaximumDelay最长保留时间硬上限8.0s2.0s源码WebKitTeardown.swift L12-L24。可以看到 Intel 机型的差异点集中在两处清理后不立即关闭窗口改orderOut以及把最长保留时间放大到 8 秒——对应文档“Delays teardown so WebKit can unwind callbacks”与“Keeps owners retained briefly on x86_64 to avoid autorelease crashes”两条说明。公开 API 与清理时间线scheduleCleanup(owner:window:webView:closeWindow:)L30-L51的行为是把owner存入retained保留表以ObjectIdentifier为键并用scheduled集合做幂等保护——同一 owner 重复调用只会调度一次通过Task { MainActor in ... }先Task.yield()让出主线程再sleep0.25s即文档所说的“延迟 teardown 让 WebKit 展开回调”调用私有cleanup(window:webView:closeWindow:)L68-L83webView?.stopLoading()→ 置空navigationDelegate与window?.delegate然后按平台分支x86_64 上执行window?.orderOut(nil)只隐藏不关闭其他架构则执行调用方闭包或window?.close()最后scheduleReleaseL85-L102进入轮询循环每 0.2s 检查一次window?.isVisible与webView?.isLoading只有当“已静默两者都为 false且至少过了 2 秒”或“达到最大保留时长2s/8s”时才从retained表中移除 owner完成真正的释放。轮询的意义在于即使延迟期内 WebKit 又触发了窗口可见或加载回调比如购买页的异步请求回来了也会继续保留对象避免释放过早。另一个公开方法是retain(_ owner:)L26-L28只做“登记保留”而不触发清理供调用方自行控制后续关闭时机时使用。此外源码提供了#if DEBUG的测试钩子isRetainedForTesting/isScheduledForTesting/resetForTestingL53-L66并在每一步通过CodexBarLog.logger(LogCategories.webkitTeardown)记录结构化日志“WebKit cleanup scheduled” → “WebKit cleanup” → “WebKit cleanup released”方便排查 teardown 时序问题。测试对行为的验证WebKitTeardownTests.swift 验证了调度幂等性与测试钩子调用scheduleCleanup后 owner 同时处于 retained 和 scheduled 状态resetForTesting()后两者归零OpenAIDashboardWebViewCacheTests.swift如 L887-L994验证了离屏宿主close()之后WebKitTeardown.isScheduledForTesting(host)为真、重复关闭不会二次调度与前面isClosed幂等保护 scheduled集合去重相互印证。Cookie 存储策略nonPersistent 与按账号持久化docs/webkit.md 的 Notes 第一条建议是“对于手动持久化 Cookie 的登录流优先使用WKWebsiteDataStore.nonPersistent()”。仓库内可以对照到两类实践临时/scratch 存储用 nonPersistentOpenAIDashboardBrowserCookieImporter.swift L610 中let scratch WKWebsiteDataStore.nonPersistent()用于导入 Cookie 的中间过程避免污染默认数据存储测试侧 OpenAIDashboardFetcher.swift L328-L329 也显式用.nonPersistent()构造缓存条目需要跨会话保持登录态的窗口用按账号的持久化存储购买窗口即上文提到的OpenAIDashboardWebsiteDataStore.store(forAccountEmail:scope:)保证同一账号重开窗口时 Cookie 仍在。可以推断文档之所以建议登录流“手动管 Cookie nonPersistent”是为了让 Cookie 的生命周期由应用自己而非 WebKit 默认数据目录掌握便于多账号隔离与清理。实践清单把 docs/webkit.md 的规则和源码实现合并CodexBar 内新增或修改 WebKit 窗口时应遵循任何“流程结束即关闭”的 WebKit 窗口登录、购买等以及抓取后隐藏/关闭的离屏宿主清理一律走WebKitTeardown.scheduleCleanup(owner:window:webView:closeWindow:)不要直接关闭 WebView延迟销毁期间保持window.isReleasedWhenClosed false否则close()会提前释放窗口后续orderOut/关闭闭包会触碰悬空对象手动管理 Cookie 的登录流优先WKWebsiteDataStore.nonPersistent()需要保留登录态的宿主则使用按账号隔离的持久化存储修改窗口生命周期行为前先读 WebKitTeardown.swiftIntel 与 Apple Silicon 的常量分叉retainAfterCleanup、releaseMaximumDelay意味着在 x86_64 上“关闭隐藏最长 8 秒后释放”在 Apple Silicon 上是“关闭最短 2 秒释放”调试 teardown 崩溃文档特别点名 x86_64时应重点核对这些阈值与轮询逻辑。以上所有路径均为当前仓库内的相对路径可直接打开对照核心实现 Sources/CodexBarCore/WebKit/WebKitTeardown.swift两处调用方 Sources/CodexBar/OpenAICreditsPurchaseWindowController.swift、Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardWebViewCache.swift离屏窗口几何参数 Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardFetcher.swift以及测试 Tests/CodexBarTests/WebKitTeardownTests.swift。【免费下载链接】CodexBarShow usage stats for OpenAI Codex and Claude Code, without having to login.项目地址: https://gitcode.com/GitHub_Trending/co/CodexBar创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考