定時(shí)器(KeyRepeatTimer)的時(shí)序守衛(wèi)設(shè)計(jì):可見性錨點(diǎn)與遲到判定規(guī)則解析)
AltTab macOS 人工按鍵重復(fù)定時(shí)器KeyRepeatTimer的時(shí)序守衛(wèi)設(shè)計(jì)可見性錨點(diǎn)與遲到判定規(guī)則解析【免費(fèi)下載鏈接】alt-tab-macosWindows alt-tab on macOS項(xiàng)目地址: https://gitcode.com/gh_mirrors/al/alt-tab-macos導(dǎo)讀AltTabalt-tab-macos在純修飾鍵快捷鍵如按住 ? 再連按 ?下系統(tǒng)不會(huì)產(chǎn)生 OS 級(jí)按鍵重復(fù)因此必須在應(yīng)用內(nèi)合成一套“人工按鍵重復(fù)”定時(shí)器讓按住快捷鍵時(shí)選擇框持續(xù)循環(huán)推進(jìn)。本篇文章以KeyRepeatTimerSpecs.md為骨架結(jié)合 KeyRepeatTimer.swift、KeyRepeatTimerTestable.swift 與 KeyRepeatTimerTests.swift 的源碼與測試完整講解這套定時(shí)器的三大核心難題與解法初始延遲寬限期從“面板真正可見”起算、雙錨點(diǎn)可見時(shí)間戳/顯示時(shí)間戳的選取邏輯、以及“遲到 tick 一律拒絕”的單發(fā)重武裝機(jī)制。讀完本文你將掌握這套決策內(nèi)核的完整判定規(guī)則、邊界常數(shù)1s 兜底預(yù)算、20ms 下限及其背后的實(shí)測 bug 根因。一、為什么需要人工按鍵重復(fù)1.1 系統(tǒng)不重復(fù)應(yīng)用來合成普通帶 keycode 的快捷鍵如 ??由系統(tǒng)直接產(chǎn)生 auto-repeat 的 keyDown 事件無需額外處理。但純修飾鍵組合例如 ??其中 ? 在 ? 被按住時(shí)沒有獨(dú)立 keycode不會(huì)觸發(fā) OS 級(jí)重復(fù)。因此 AltTab 需要自己合成重復(fù)每次按住快捷鍵時(shí)用一個(gè)后臺(tái)定時(shí)器按用戶設(shè)置的KeyRepeat速率周期性地推進(jìn)選擇框。1.2 觸發(fā)與停止的調(diào)用鏈定時(shí)器的啟停由兩個(gè)入口控制啟動(dòng)KeyRepeatTimer.startRepeatingKeyPreviousWindow()/startRepeatingKeyNextWindow()在 App.swift、App.swift 處被調(diào)用分別對(duì)應(yīng)“上一個(gè)窗口”與“下一個(gè)窗口”兩條人工重復(fù)鏈路停止KeyRepeatTimer.stopTimerForRepeatingKey(_:)在 ATShortcut.swift快捷鍵狀態(tài)轉(zhuǎn)為up時(shí)和 TilesView.swift切換器關(guān)閉時(shí)被調(diào)用。啟動(dòng)時(shí)有兩條前置約束源碼見 KeyRepeatTimer.swift僅對(duì)無 keycode 的快捷鍵生效previousWindowShortcut要求shortcut.keyCode .noneEsc 鍵被排除Esc 通過 cghid 事件通道#5585送達(dá)會(huì)產(chǎn)生真實(shí) OS 重復(fù)若再疊加人工定時(shí)器被吸收的 keyDown 會(huì)讓 Carbon 收不到配套的釋放事件定時(shí)器永不停止選擇框會(huì)一路循環(huán)到列表末尾#5742。定時(shí)器使用DispatchSource.makeTimerSource(queue: BackgroundWork.repeatingKeyQueue.strongUnderlyingQueue)跑在 BackgroundWork.swift 中定義的專用 OperationQueuerepeatingKey并發(fā)度為 1上與主線程隔離。二、初始延遲寬限期的意義2.1 速率參數(shù)的來源每次武裝arm定時(shí)器時(shí)AltTab 會(huì)讀取 macOS 全局默認(rèn)值let repeatRate ticksToSeconds(CachedUserDefaults.globalString(KeyRepeat) ?? 6) let initialDelay ticksToSeconds(CachedUserDefaults.globalString(InitialKeyRepeat) ?? 25)其中ticksToSeconds將系統(tǒng)的“tick”數(shù)換算為秒Apple 硬編碼60 ticks 1 秒且在高刷新率顯示器如 120 FPS上依舊如此KeyRepeatTimer.swift。也就是說macOS 默認(rèn)的InitialKeyRepeat25 ticks ≈ 417ms與KeyRepeat6 ticks ≈ 100ms會(huì)被換算為秒級(jí)時(shí)間戳參與判定。2.2 為什么寬限期必須從“可見”起算第一次武裝定時(shí)器后首個(gè) tick 計(jì)劃在armedAt initialDelay觸發(fā)。但問題在于如果用戶在全屏/Space 切換后立即喚起切換器WindowServer 正忙于收尾切換動(dòng)畫無法立刻繪制 AltTab 的.canJoinAllSpaces面板——面板像素可能要到定時(shí)器武裝后約 500ms 才真正上屏。此時(shí)后臺(tái)DispatchSource仍在持續(xù) tick隊(duì)列中積壓的 tick 會(huì)在面板出現(xiàn)的瞬間全部觸發(fā)選擇框在用戶毫無輸入的情況下一次跳過多格。因此正確的語義是初始延遲寬限期必須從“面板真正被用戶看到”的時(shí)刻起算而不是從定時(shí)器武裝時(shí)刻起算。普通快速喚起不受影響面板通常在武裝后幾十毫秒內(nèi)可見。三、丟失的錨點(diǎn)一次 1.4 秒的實(shí)測回歸3.1 可見信號(hào)為何永遠(yuǎn)收不到最初方案只錨定SwitcherSession.panelBecameVisibleAt即面板自身 WindowServerorderedIn事件。但實(shí)測2026-07-30發(fā)現(xiàn)該通知從不到達(dá)ordered-in 只對(duì)WindowServerEvents.wsWindows逐窗口選擇加入opt-in集合中的 wid 投遞而面板并不在該集合中自 Sequoia 起為強(qiáng)制要求。于是每個(gè) tick 都落入兜底分支按住循環(huán)實(shí)際上從armedAt initialDelay 1s才開始——實(shí)測首跳1377ms而系統(tǒng)InitialKeyRepeat僅417ms每一次按住都受影響。日志佐證Carbon 熱鍵每次按下只觸發(fā)一次、忽略 auto-repeat keyDown14 次自動(dòng)重復(fù) keyDown 只產(chǎn)生了恰好一次state:down證明人工重復(fù)是按住期間推進(jìn)選擇的唯一途徑。3.2 為什么不把面板加入 opt-in 集合直觀的修復(fù)是把面板加進(jìn)wsWindows但源碼明確拒絕了這個(gè)方案SwitcherSession.swift那會(huì)把面板自身的 order/geometry 事件作為“未跟蹤 wid 的輸入”喂進(jìn) reducer對(duì)一個(gè)“時(shí)序細(xì)節(jié)”而言風(fēng)險(xiǎn)過大。3.3 解決方案不可能丟失的panelShownAt替代錨點(diǎn)由 TilesPanel.swift 的show()提供在makeKeyAndOrderFront(nil)后立即寫入session.panelShownAt ProcessInfo.processInfo.systemUptime每次召喚只寫一次避免同一次會(huì)話內(nèi)重新顯示時(shí)在用戶手指下重啟寬限期。該錨點(diǎn)不可能丟失但略微偏早——orderFront返回后 WindowServer 才真正繪制。因此可見信號(hào)panelBecameVisibleAt依然保留作為“校正層”高于顯示時(shí)間戳而非被替換兩者同時(shí)存在時(shí)以更晚、更真實(shí)的可見時(shí)間為準(zhǔn)測試testTheWindowServerSignalOverridesTheShowAnchor驗(yàn)證。代價(jià)也在源碼注釋中明確陳述在真正緩慢的顯示場景下寬限期從 order-front 而非 paint 起算可能讓一次提前推進(jìn)落地——這是“不讓每次普通召喚都晚 1 秒”換來的取舍。四、顯示之后的卡頓#59774.1 癥狀與根因可見性錨點(diǎn)只能覆蓋“面板呈現(xiàn)之前”的卡頓呈現(xiàn)之后若發(fā)生卡頓破壞力相同而錨點(diǎn)看不見它。實(shí)測復(fù)現(xiàn)閑置 5–10 分鐘后第一次 alt-tab選擇框落在列表約第 8 格而非上一個(gè)窗口——這個(gè)數(shù)量由流逝時(shí)間與重復(fù)速率決定與窗口集合無關(guān)。兩個(gè)設(shè)計(jì)共同制造了積壓定時(shí)器按重復(fù)周期調(diào)度在自有后臺(tái)隊(duì)列上循環(huán)觸發(fā)。主線程繁忙時(shí)每個(gè)錯(cuò)過的間隔都堆積一個(gè)排隊(duì) block停止定時(shí)器的 key-up 是事件源run loop 會(huì)先排空整個(gè)主 dispatch 隊(duì)列、再讀取下一個(gè)事件。因此積壓的 tick 總是先于取消它的釋放事件被執(zhí)行且每個(gè) block 重新檢查now此時(shí)寬限期早已滿足。4.2 兩項(xiàng)修復(fù)單發(fā) 處理完畢后自重新武裝scheduleTick每次只排一個(gè) deadlinehandleEvent在主線程處理完當(dāng)前 tick 后才調(diào)用scheduleNextTick重新調(diào)度下一發(fā)KeyRepeatTimer.swift。任何時(shí)刻最多只有一個(gè) tick 在途卡頓只會(huì)推遲下一個(gè) tick 而不是堆積更多。代價(jià)是間隔變?yōu)椤癶andler 到 handler”而非“fire 到 fire”每個(gè)周期漂移一個(gè)處理周期亞毫秒級(jí)相對(duì) 33–500ms 的KeyRepeat可忽略。遲到即拒絕tick 到達(dá)主線程太晚時(shí)直接丟棄——一個(gè)等待過的 tick 不足以證明按鍵仍然按住詳見第五節(jié)。此 bug 在 v11.4.4 之前一直被掩蓋完全沒有錨點(diǎn)時(shí)每個(gè) tick 都走initialDelay 1s兜底反而吞掉了短于約 1.4s 的積壓。五、決策內(nèi)核完整判定規(guī)則5.1 純函數(shù)內(nèi)核判定邏輯被抽取為KeyRepeatTimerTestable.shouldApplyArtificialRepeat(now:firedAt:armedAt:panelBecameVisibleAt:panelShownAt:initialDelay:repeatRate:)KeyRepeatTimerTestable.swift所有輸入都是systemUptime時(shí)間戳無定時(shí)器、無 DispatchSource、無 AppKit 依賴因此可以純單元測試。核心實(shí)現(xiàn)僅四行if now - firedAt lateBudget(repeatRate) { return false } if let anchor panelBecameVisibleAt ?? panelShownAt { return now - anchor initialDelay } return now - armedAt initialDelay missedVisibleSignalBudget5.2 三條規(guī)則詳解規(guī)則條件結(jié)果1. 遲到判定now - firedAt max(repeatRate, 20ms)拒絕無論錨點(diǎn)如何2. 錨點(diǎn)寬限期錨點(diǎn)已知panelBecameVisibleAt ?? panelShownAt當(dāng)且僅當(dāng)now - anchor initialDelay時(shí)應(yīng)用WindowServer 可見時(shí)間戳優(yōu)先3. 兜底兩者均未知等到armedAt initialDelay 1s后照常應(yīng)用關(guān)鍵邊界說明missedVisibleSignalBudget 1秒KeyRepeatTimerTestable.swift保證可見信號(hào)缺失時(shí)按住循環(huán)不會(huì)永久卡死。它從“所有 tick 的必經(jīng)路徑”變成了“真正的安全網(wǎng)”。遲到預(yù)算下限 20msminimumLateBudget同文件第 13 行因?yàn)閐efaults write -g KeyRepeat 0是合法配置系統(tǒng)視作“重復(fù)間無延遲”。若預(yù)算允許為零規(guī)則 1 會(huì)拒絕每一個(gè) tick把按住循環(huán)徹底鎖死。邊界取可見時(shí)間恰好等于initialDelay前時(shí)即應(yīng)用測試testAppliesExactlyAtInitialDelayBoundary驗(yàn)證。實(shí)際調(diào)用時(shí)firedAt在定時(shí)器 handler 觸發(fā)瞬間捕獲now在跳轉(zhuǎn)到主線程之后捕獲KeyRepeatTimer.swift兩者之差即“主線程排隊(duì)延遲”。六、測試場景全景與KeyRepeatTimerTests.swift一一對(duì)應(yīng)KeyRepeatTimerSpecs.md的測試場景小節(jié)與 KeyRepeatTimerTests.swift 逐條對(duì)應(yīng)以下按分組整理測試固定initialDelay0.4s、repeatRate0.1s用now與各時(shí)間戳的相對(duì)位置構(gòu)造場景6.1 A2 組顯示時(shí)刻錨點(diǎn)testAppliesFromTheShowAnchorWhenNoWindowServerSignalArrives——1.4s bug 的回歸測試無 WindowServer 信號(hào)時(shí)寬限期從panelShownAt起算而非arm 1stestTheWindowServerSignalOverridesTheShowAnchor——兩者同時(shí)已知時(shí)可見信號(hào)優(yōu)先panelBecameVisibleAt: 99.9晚于panelShownAt: 99.1拒絕應(yīng)用緩慢顯示不能提前開啟寬限期。6.2 A 組可見時(shí)間戳已知——從可見性門控testAppliesOnceVisibleForInitialDelay——可見 0.5s 前、延遲 0.4s → 應(yīng)用testSkipsWhenNotVisibleLongEnough——可見 0.1s 前、延遲 0.4s → 跳過核心修復(fù)慢顯示已呈現(xiàn)面板但自可見以來的寬限期未滿testAppliesExactlyAtInitialDelayBoundary——可見恰好initialDelay前 → 應(yīng)用邊界。6.3 B 組可見時(shí)間戳未知——arm 相對(duì)兜底testSkipsBeforeFallbackBudgetWhenNeverVisible——從未可見、武裝 0.5s 前、延遲 0.4s → 跳過排隊(duì)爆發(fā)場景面板未上屏?xí)r抑制應(yīng)于~arminitialDelay到期的重復(fù)兜底僅在initialDelay 1s后打開testAppliesAfterFallbackBudgetWhenNeverVisible——從未可見、武裝 1.5s 前、延遲 0.4s → 應(yīng)用 0.4 1可見信號(hào)缺失不會(huì)永久卡死按住循環(huán)。6.4 C 組tick 遲到主線程testSkipsATickThatWaitedLongerThanOneRepeatInterval——#5977 回歸fired 0.6s 前才執(zhí)行 → 拒絕testAppliesATickThatReachedMainPromptly——普通的微秒級(jí)主線程跳轉(zhuǎn)仍然正常循環(huán)testTheLateBudgetIsOneRepeatInterval——較慢的KeyRepeat容忍成比例的更長等待同一firedAt差值下repeatRate0.1拒絕、0.5應(yīng)用testAZeroRepeatRateStillAppliesPromptTicks——KeyRepeat 0下 20ms 下限仍讓及時(shí) tick 應(yīng)用而明顯遲到者0.1s依然被拒。七、從源碼看設(shè)計(jì)啟示時(shí)序問題用“單一時(shí)鐘 相對(duì)差”建模所有判定統(tǒng)一使用ProcessInfo.processInfo.systemUptime時(shí)間戳的差值避免混用絕對(duì)時(shí)間與不同時(shí)鐘源。純決策與副作用徹底分離判定規(guī)則放在KeyRepeatTimerTestable純枚舉中定時(shí)器與 AppKit 副作用留在KeyRepeatTimer使 10 個(gè)時(shí)序邊界場景能被無定時(shí)器地快速單測?!板^點(diǎn)可能缺失”要當(dāng)成一等公民panelBecameVisibleAt理論精確卻實(shí)際永遠(yuǎn)收不到panelShownAt必然存在卻略早——用“精確者優(yōu)先、可靠者兜底”的雙錨點(diǎn) 1s 安全網(wǎng)覆蓋全部路徑而不是賭某個(gè)信號(hào)一定會(huì)到。積壓永遠(yuǎn)優(yōu)先于取消事件的 run loop 語義是 #5977 的底層原因解決方式不是“更快處理”而是讓定時(shí)器從“重復(fù)周期”變?yōu)椤皢伟l(fā)自再武裝”從結(jié)構(gòu)上消除多 tick 在途的可能。八、延伸閱讀決策內(nèi)核與規(guī)則注釋KeyRepeatTimerTestable.swift定時(shí)器武裝、handler 與再調(diào)度實(shí)現(xiàn)KeyRepeatTimer.swift全部時(shí)序邊界測試KeyRepeatTimerTests.swift會(huì)話級(jí)錨點(diǎn)字段語義SwitcherSession.swiftpanelShownAt的寫入點(diǎn)TilesPanel.swiftpanelBecameVisibleAt的當(dāng)前不觸發(fā)的寫入邏輯WindowServerEvents.swift定時(shí)器專用后臺(tái)隊(duì)列BackgroundWork.swift【免費(fèi)下載鏈接】alt-tab-macosWindows alt-tab on macOS項(xiàng)目地址: https://gitcode.com/gh_mirrors/al/alt-tab-macos創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考