Skip to content

Commit c42085b

Browse files
committed
MainAssembly // Refactor session mgmt., deprecating NSMapTable.
1 parent cfb8211 commit c42085b

2 files changed

Lines changed: 66 additions & 12 deletions

File tree

Packages/vChewing_MainAssembly4Darwin/Sources/MainAssembly4Darwin/SessionController/InputSession.swift

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

218267
extension InputSession {

Packages/vChewing_MainAssembly4Darwin/Sources/MainAssembly4Darwin/SessionController/SessionCtl.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,15 @@ public final class SessionCtl: IMKInputSessionController {
7474
// 參見 DevLab/InputMethodKitPhuquingRetarded.txt 內的分析。
7575
let maybeClientOnMain = maybeClient as? NSObject
7676
let clientObj = maybeClientOnMain ?? (client() as? NSObject)
77-
if let clientObj, let cached = InputSession.cachedSession(for: clientObj) {
78-
cached.reassign(to: self, clientProvider: getClientProvider())
79-
vCLog("InputSession reused. ID: \(cached.id.uuidString)")
80-
return cached
77+
// 改用 uniqueClientIdentifierString 作為快取鍵。
78+
// 此舉解決 Chrome/Electron 的 client object memAddr 不穩定問題。
79+
if let clientObj {
80+
let key = Int(bitPattern: Unmanaged.passUnretained(clientObj).toOpaque())
81+
if let cached = InputSession.cachedSession(for: key) {
82+
cached.reassign(to: self, clientProvider: getClientProvider())
83+
vCLog("InputSession reused. ID: \(cached.id.uuidString)")
84+
return cached
85+
}
8186
}
8287
// 先用傳入的參數完成 InputSession 的初期化,其中包括了對這個 Session 的登記過程。
8388
let newSession = InputSession(controller: self) {

0 commit comments

Comments
 (0)