Skip to content

perf: cache keyboard rows and skip redundant layout reloads - #44

Merged
fxliang merged 2 commits into
fxliang:fxfrom
SandyYuR:fx
Aug 16, 2026
Merged

perf: cache keyboard rows and skip redundant layout reloads#44
fxliang merged 2 commits into
fxliang:fxfrom
SandyYuR:fx

Conversation

@SandyYuR

@SandyYuR SandyYuR commented Aug 15, 2026

Copy link
Copy Markdown

问题背景

每次按键(macrokey“切层”、Shift 切换中英文)都会把整个键盘视图树拆掉重建。同一布局在小米 14 上一次 reloadLayout 只要 528ms,
而在一加 12(ColorOS 16)上要 121
561ms,导致切层和中英文切换有明显的零点几秒卡顿。

本 PR 的核心思路:按“布局/样式签名”把已构建好的行容器缓存起来,后续重建直接复用,跳过昂贵的视图构造;
同时去掉几个冗余的重建/窗口重布局触发点。

改动内容

  • 按布局/样式签名缓存已构建的行容器,后续 reload 直接复用。
    • 组合输入状态变化(onCompositionStateChanged 会原地重建 compose 视图)之前,先失效当前签名对应的缓存条目,
      保证被原地修改过的行永远不会以过期外观被复用;
    • 字体热刷新(checkAndApplyFontRefresh 消费刷新标志后)之前,先清空行缓存,保证新字体真正被重新应用;
    • 仅当缓存行确实由同一批 KeyDef 实例构建时才复用;对每次调用都重建 KeyDef 的提供方(如无布局文件时的内置兜底布局),
      回退为整树重建,而不是静默跳过状态重注册。
  • 抽出 selectedLayoutArray 并显式分组,避开 ?:?. 的优先级陷阱——扁平布局在中英文切换之间保持稳定签名,
    shift 切中英文不再触发任何重建。
  • 切层后记录已应用的布局签名,跳过紧随其后的第二次冗余重建。
  • 只有键盘高度真正变化时才通知 IME 窗口更新高度,避免每次按键都走昂贵的窗口重布局路径。
  • 子模式布局扫描结果按布局修订号记忆化。

实测数据

一加 12(ColorOS 16),与小米 14 上相同的自定义布局:

  • 修改前:每次切层/切换中英文都会触发两次完整 reloadLayout,每次 121~561ms;
  • 修改后:切层 8~16ms(reuse=true,仅每层首次进入构建一次),shift 切中英文完全不重建。

测试矩阵

  • ColorOS 16(一加 12):切层、Shift 切中英文——真机验证,日志确认(reuse=true,8~16ms)
  • MIUI(小米 14):同一布局——真机验证
  • 字体热加载(设置里改字体/字号后回到输入法界面)
  • 键盘打开状态下在布局编辑器里保存
  • 分屏键盘开关
  • 主题切换
  • 横竖屏切换

备注

  • 与先前讨论一致:独立 Shift 按键保持时长维持上游原样(150ms),本 PR 不包含该行为变更。
  • 高度通知的 gating 逻辑上只影响高度依赖的路径,但 updateKeyboardSize 还附带单手模式/悬浮条等副作用,建议合入前按需真机复核。

@SandyYuR
SandyYuR marked this pull request as draft August 15, 2026 17:35
@SandyYuR
SandyYuR marked this pull request as ready for review August 16, 2026 01:44
@fxliang
fxliang requested a lite review from Copilot August 16, 2026 02:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reduces input latency by caching prebuilt keyboard row containers keyed by a layout/style signature, and by avoiding redundant layout reloads / IME-window relayout triggers during frequent state transitions (layer switching, shift-based language toggles, font refresh).

Changes:

  • Add per-signature reusable row caching in BaseKeyboard.reloadLayout() and cache invalidation hooks (composition changes, font refresh).
  • Refactor TextKeyboard layout signature computation (including sub-mode sensitivity detection + memoization) and mark applied signatures to skip redundant rebuilds.
  • Gate onKeyboardHeightSourceChanged() notifications to only run when the resolved keyboard height override actually changes.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt Cache and reuse built row containers per layout/style signature; invalidate cache on composition changes; expose cache clearing API.
app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/TextKeyboard.kt Compute more stable layout signatures (optionally excluding sub-mode), memoize sub-mode usage scanning, and track applied signatures.
app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt Clear row cache on font refresh; avoid redundant IME-window relayout calls when keyboard height doesn’t change.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Layer switches (macrokey "layer to") and shift-based language toggles used to
rebuild the entire keyboard view tree on every key press; on ColorOS/OnePlus a
single reloadLayout cost 120-560ms (vs 5-28ms on MIUI).

- Cache built rows per layout/style signature and reuse them on subsequent
  reloads. Cached rows are evicted before composition-state changes mutate them
  in place, and before font-set refreshes whose flag was already consumed, so
  cached rows are never reused with stale appearance.
- Reuse only when the cached rows were built from the exact same KeyDef instances;
  providers that re-create defs on every call (e.g. the builtin fallback layout)
  fall back to a rebuild instead of silently re-registering mismatched state.
- Extract selectedLayoutArray with explicit grouping to avoid the ?:/?.
  precedence trap, so flat layouts keep a stable signature across shift language
  toggles and no longer trigger any rebuild.
- Record the applied layout signature after a layer switch to skip the
  redundant second rebuild.
- Notify the IME window of keyboard height changes only when the height
  actually changed.
- Memoize the sub-mode layout scan per layout revision.

Measured on OnePlus 12 (ColorOS 16): repeated layer switches complete in ~8-16ms
with cached rows (previously 121-561ms per rebuild); shift language toggles no
longer rebuild at all.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (2)

app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/BaseKeyboard.kt:227

  • currentLayoutSignature() is now part of the correctness boundary for row reuse, but it defaults to an empty string. Any future BaseKeyboard subclass that changes its KeyDef layout without overriding this will silently risk reusing the wrong cached rows. Consider making this method abstract (or otherwise enforcing/deriving a non-empty, layout-dependent signature) to prevent accidental misuse.
    /**
     * Signature identifying the semantic keyboard content (layer, input method, sub mode, ...).
     * Subclasses rendering different KeyDef sets must override this so cached rows are never
     * reused across different layouts.
     */
    protected open fun currentLayoutSignature(): String = ""

app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt:257

  • In switchLayout(), the target == currentKeyboardName fast-path still triggers onKeyboardHeightSourceChanged() whenever notifyHeightChange=true. Calls like consumeOneShotLayerIfNeeded() typically end up here (switching to TextKeyboard when it’s already current) and can still cause an expensive updateKeyboardSize() even when TextKeyboard.currentLayoutHeightPercentOverride() didn’t change.
                        updateCompositionState()
                    }
                    applyAuxActions(lastAuxActions)
                    if (notifyHeightChange) {
                        service.inputView?.onKeyboardHeightSourceChanged()
                    }

- Include splitGapPercent and expandKeypressArea in the row-cache
  signature so split-gap / key-area preference changes are re-applied
  instead of reusing rows built with the old values.
- registerReusableRowState validates every row/key before touching the
  space/compose registries (two-phase), and returns false on any mismatch
  so reloadLayout falls back to a full rebuild without leaving partially
  registered state behind.
- Only SpaceKey is registered into spaceKeys on reuse, matching the
  fresh-build path (MiniSpaceKey was previously added on reuse only).
- Make currentLayoutSignature() abstract so every subclass must provide one,
  preventing future dynamic-layout subclasses from silently reusing wrong rows.
- Replace the whole-cache clear with LRU eviction (access-ordered
  LinkedHashMap, capped at 12 entries).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@SandyYuR SandyYuR left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

来自涨价后的DeepSeek-V4-Pro的评论

@fxliang
fxliang merged commit c6124ac into fxliang:fx Aug 16, 2026
5 checks passed
@SandyYuR
SandyYuR deleted the fx branch August 17, 2026 01:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants