很多中文打字用户,无论用原厂输入法、还是任何一款副厂输入法,都会有中英文混打的需求。
十几年前开始自从 macOS 10.12 Sierra 引入基于 CpLk 的中英文输入法切换功能以来,这个问题到现在就都没消停过:「敲 CpLk 切换中英文输入法时会卡顿」。每台电脑跑每个版本的 macOS 时的发病严重程度不一:有的可以忽略,有的卡到骂娘。
长久以来,对此问题的讨论,往往被归咎于输入法本身:要么是在用的副厂输入法本身被抱怨,要么是原厂中文输入法本身被抱怨。
其实,这是 InputMethodKit 的一个堪称陈年大粪的设计缺陷。
笔者是唯音输入法的开发者,本文只是经验谈。谢绝未经授权的转载。
笔者按顺序罗列这些事实。先讲两个技术层面的:
- InputMethodKit 的 IMKInputController 所有 API 呼叫都是在 MainActor 上的。然而,因为 ObjC Header 层面曝露的 API 与 Swift Concurrency 不相容的缘故,如果你要给输入法做 Swift Concurrency Modernization 的话,你需要一些 Dirty Trick 方便将这些 API 传入的参数重新从 MainActor 强制解读。
- macOS 10.7 开始引入 Objective-C ARC。这套 ARC 系统对 NSObject 副本的析构时机不可控,你无法用手动介入的方式使其暂缓析构或重新利用。而 InputMethodKit 的 API 在设计时是针对 macOS 10.5 Leopard 设计的。这就带来了一些与 ARC 有关的 MainActor 调度压力问题(甚至阻塞)。下文会提到具体的问题情形。
再来讨论使用者打字场景上的事实:
- macOS 10.12 的这个 CpLk 切换功能的本质不是中英文打字模式切换,而是输入法切换。
- macOS 哪怕英文打字也是由一个专门的输入法负责的。大部分英语键盘的电脑上,这个输入法叫 Apple ABC,对应美规键盘。
- 每个输入法在刚被切换出来时,会触发这个输入法自身的 IMKInputController Instance 的创建以及其 activateServer 操作(以及可能有的一系列追加操作)。然后才是这个 Client 之前对接的输入法的 IMKInputController 副本的 Deactivation。
- 很多中英文混合打字的用户经常会在 ABC 与中文输入法之间来回切换。由于这种情况下两者所服务的 IMKTextInput Client 是相同的,所以就出现了 MainActor 塞车。而且,过于高频的来回切换,会给 IMKInputController 所用的 Objective-C ARC 带来压力。ARC 废件释放与物件交互都发生在 MainActor 上,必然会发生塞车。
- 「在同一个 client 切换输入法」的过程会牵涉到前后两个 IMKInputController 副本各自的对 client() 的操作。输入法开发者现在最佳的范式就是让 deactivateServer 在 MainActor 上 Async 脱手操作、且不在 deactivateServer 阶段做 client() API 的文字写入/内容显示交互,因为这种擦除操作会由系统代劳。但是,这个由系统代劳的擦除操作也是发生在 MainActor 上的。这就出现了 MainActor 的任务的时序冲突。InputMethodKit 内部应该是有自己的方式处理这个冲突,然而代价就是阻塞开销。
- 有些输入法难免会在 activateServer 阶段引入与 client() 有关的交互,但这个开销可能在所难免,因为你可能必须得对 client 套用指定的 Ukelele 布局。再加上 client() 身为 IMKTextInput Client 没有真正意义上的 Async API,输入法开发者只能假设所有这类 Client 的这些操作都是 MainActor 阻塞操作,然后干瞪眼。
这就导致那些经常用 CpLk 超高频中英切换打字的使用者们必然会骂娘。但他们不知道问题烂在系统层面,于是就只能骂输入法。或骂系统内建注音烂,或骂自己在用的副厂输入法不修故障。
这个现状恐怕真的没有解决方法,要淘汰的是 macOS 的整个 InputMethodKit 体系。整个 API 交互体系都需要刮骨疗毒重新设计。
或者,自力救济(下述几条都很重要):
- 所有 macOS 中文用户都请关掉以 CpLk(「中/英」键)切换中英文输入法的特性,让系统遵循 macOS 10.11 El Capitan 为止的 CpLk 行为。
- 中文输入法开发者们都请集体给自己的输入法实装 CpLk 英打模式。英打模式下的 Layout 同步问题也可以用 client().overrideKeyboard() 来解决,不过这是个阻塞操作。而且,如果接收打字的视窗是输入法自己的视窗的话,务必 MainActor Async 执行
client().overrideKeyboard(),否则必然会卡死几十秒。 - IMKInputController Subclass 不要拥有任何物件。所有与打字有关的业务模组全部塞到 singleton 或者可以复用的 instance 里面。instance 与 IMKInputController Subclass 之间可以使用其他通讯交互手段。
P.S.: macOS 26 在 AppKit / InputMethodKit 的 NSObject Types 的 ARC 回收方面的效率低下之故障反而放大了这个问题。
$ EOF.