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