@@ -185,19 +185,40 @@ public final class InputSession: @MainActor SessionProtocol, Sendable {
185185
186186 // MARK: Internal
187187
188- /// 從快取中查詢既有的 InputSession(以 client NSObject 的記憶體位址為鍵)。
189- static func cachedSession( for clientObj: NSObject ) -> InputSession ? {
190- sessionsByClient. object ( forKey: clientObj)
188+ // LRU 記數器,用於 sessionsByClient 的 LRU 淘汰。
189+ var lruTick : UInt = 0
190+
191+ /// 從快取中查詢既有的 InputSession(以 client NSObject 的記憶體位址整數值為鍵)。
192+ static func cachedSession( for memAddr: Int ) -> InputSession ? {
193+ sessionsLock. withLock {
194+ guard let session = sessionsByClient [ memAddr] else {
195+ return nil
196+ }
197+ // 孤本清理:若快取中的 session 已無任何 SessionCtl 參照,立即移除。
198+ if session. inputControllerAssigned == nil ,
199+ Self . current? . id != session. id {
200+ sessionsByClient [ memAddr] = nil
201+ return nil
202+ }
203+ // 命中時更新 LRU 記數器(用於 count > 5 時的批量淘汰)。
204+ session. lruTick = Self . globalLRUTick
205+ Self . globalLRUTick &+= 1
206+ return session
207+ }
191208 }
192209
193210 /// 將自身登記至快取。首次建構 InputSession 時呼叫。
194211 func registerInCache( ) {
195212 guard let clientObj = theClient ( ) else { return }
196- Self . sessionsByClient. setObject ( self , forKey: clientObj)
213+ let key = Int ( bitPattern: Unmanaged . passUnretained ( clientObj) . toOpaque ( ) )
214+ Self . sessionsLock. withLock {
215+ Self . sessionsByClient [ key] = self
216+ }
217+ // 登記後觸發 LRU 檢查(僅在 count > 5 時實際淘汰)。
218+ Self . evictLRUIfNeeded ( )
197219 }
198220
199221 /// 重新綁定至新的 SessionCtl(快取命中時使用)。
200- /// 僅更新控制器參照與 clientProvider,不重新建構打字模組。
201222 func reassign( to controller: SessionCtl , clientProvider: @escaping ( ) -> ClientObj ? ) {
202223 inputControllerAssigned = controller
203224 theClient = clientProvider
@@ -209,10 +230,38 @@ public final class InputSession: @MainActor SessionProtocol, Sendable {
209230
210231 // MARK: - Session 快取 (緩解 CapsLock 高頻切換場景下的 ARC 壓力)
211232
212- /// 弱鍵快取:將 client NSObject(弱引用)映射至 InputSession(強引用)。
213- /// 當 client 被 ARC 回收後,對應條目會在下次存取時自動清除。
233+ /// 以 client NSObject 的記憶體位址整數值為鍵的快取字典。
234+ /// 取代舊版 NSMapTable——NSMapTable 自身的內部設計在 Chrome/Electron
235+ /// 等頻繁變更 client proxy 物件的場景下會導致 hang。
214236 /// - Remark: 參見 DevLab/InputMethodKitPhuquingRetarded.txt 內的分析。
215- private static var sessionsByClient = NSMapTable < NSObject , InputSession > . weakToStrongObjects ( )
237+ private static var sessionsByClient = [ Int: InputSession] ( )
238+ /// 併行存取保護鎖。
239+ private static let sessionsLock = NSLock ( )
240+ /// 靜態全域 LRU 記數器(單調遞增,&+= 溢位迴繞)。
241+ private static var globalLRUTick : UInt = 0
242+
243+ /// 惰性 LRU 淘汰:僅在快取容量超過閾值 (5) 時執行。
244+ /// 某些使用者會利用 macOS 12+ 的 CpLk 特性瘋狂切換中英輸入法,
245+ /// 快取條目可能快速累積。LRU 確保只保留最近使用的 session。
246+ private static func evictLRUIfNeeded( ) {
247+ sessionsLock. withLock {
248+ guard sessionsByClient. count > 5 else { return }
249+ var oldestKey : Int ?
250+ var oldestTick : UInt = . max
251+ for (key, session) in sessionsByClient {
252+ guard session. inputControllerAssigned == nil ,
253+ Self . current? . id != session. id
254+ else { continue }
255+ if session. lruTick < oldestTick {
256+ oldestTick = session. lruTick
257+ oldestKey = key
258+ }
259+ }
260+ if let oldestKey {
261+ sessionsByClient [ oldestKey] = nil
262+ }
263+ }
264+ }
216265}
217266
218267extension InputSession {
0 commit comments