【免费下载链接】simpleui.koplugin
A highly customizable UI plugin for KOReader that features a home screen, bottom navigation bar, top bar and desktop modules/widgets.
SimpleUI 是一款面向KOReader的高度可定制 UI 插件,它为电子书阅读器带来主页屏、底部导航栏、顶部状态栏和丰富的桌面模块。它的强大体验,靠的正是infra/sui_patches.lua(5673 行)中一套精心的monkey-patch机制——在不改动 KOReader 源码的前提下,"劫持" 并增强核心类的方法,再在卸载时逐一可靠还原。本文带你精读这套钩子体系,看懂一个插件如何做到"改得进去、收得回来"。
为什么插件需要 Monkey-Patch
KOReader 是开源阅读器,插件无法直接修改它的源代码。SimpleUI 想要:
- 在文件管理器下方加一条导航栏、调整布局尺寸;
- 让返回键 / Home 键直达"主页屏";
- 给全屏菜单注入壁纸背景、缩小高度以避开导航栏;
- 删除书籍前先把"已读完"的书记入统计。
这些需求都落在 KOReader 的核心类上,比如FileManager、UIManager、Menu。Monkey-Patch 的思路很直白:把原方法保存起来,再替换成"先做自己的事,再调用原方法"的新函数。关键在于——替换必须可逆,否则关掉插件后残留的钩子会持续干扰原行为。
核心设计:一套可复用的"可逆钩子"
SimpleUI 没有散落地orig = Foo.bar; Foo.bar = ...,而是抽象出四个小函数统一管理,全部集中在 infra/sui_patches.lua:
| 函数 | 作用 |
|---|---|
_acquireHooks(target, key, owner) | 登记"使用者"。若钩子已存在,返回nil,保证同一方法永不被包两次 |
_addHook(state, target, key, wrapped) | 用rawget取出原函数存入state.hooks,再替换target[key] |
_trackHooks(state, entries) | 为"已被赋值过的函数"补登记,方便后续移除 |
_releaseHooks(target, key, owner) | 注销使用者;当最后一个使用者离开时才真正还原,且按逆序恢复 |
还原时的逆序循环是整个设计的精髓(infra/sui_patches.lua):
for i = #state.hooks, 1, -1 do local h = state.hooks[i] if rawget(h.target, h.key) == h.wrapped then h.target[h.key] = h.orig end end这里有个关键细节:rawget(h.target, h.key) == h.wrapped先确认"现在挂在方法上的确实是我自己包的这一层",才去还原。这样即使别处又在它之上包了一层,SimpleUI 也只会拆掉自己的那一层,不会误伤他人——这是它能和别的 user-patch 共存的基础。
💡为什么状态要挂在"类"上,而不是插件实例上?因为 KOReader 每次打开/关闭书籍都会重建插件实例,而类表在整个会话中是稳定的。挂在类上,钩子才能跨越实例重建继续生效。
七大铁律:让钩子"不怕重复安装"
KOReader 会为每个宿主界面(文件管理器、阅读器)各创建一个插件实例,并在每次开书/关书时反复创建。SimpleUI 的installAll/teardownAll因此会被反复触发,且顺序不受插件控制。为此作者在文件头部写明了 7 条规则:
- 一律通过
_acquireHooks安装——已存在则返回nil,函数绝不会被包两次; - 每个包装都要登记(
_addHook或_trackHooks),以便移除; - 在
teardownAll中通过_releaseHooks释放——只有最后一个使用者离开时才拆钩子; - 钩子状态放在被 patch 的类上,绝不放在插件实例或模块局部变量;
- 包装内部通过
_live_plugin解析"当前插件",而非捕获安装时的那个实例; - 逆序释放:只在包装仍是"最外层"时才还原;
- 每次调用必须幂等,并保证临时改动的字段即便出错也要还原。
文件里有一句很重的警告:"A patch that skips these rules stacks one wrapper per instance, and its effect grows with every book opened."—— 违反规则的 patch 会每开一本书多叠一层,效果随阅读越滚越大。
关键技巧:_live_plugin告别"陈旧实例"
_acquireHooks保证"只装一次",但会带来一个新问题:那唯一一次包装捕获的plugin变量,可能早已指向一个被销毁的旧实例。SimpleUI 用一个模块级指针_live_plugin解决(infra/sui_patches.lua):
每次
patchFileManagerClass被调用时刷新_live_plugin = plugin;包装函数内部一律用_live_plugin而非闭包里那个过期的plugin。
这就像给每个钩子留了一个"最新联系人",永远指向当前存活的那个插件对象,从根上避免了"操作了一个已断连的旧实例"这类隐蔽 bug(比如导航栏高亮停在旧标签页)。
实战①:包装FileManager.setupLayout
最典型的例子是给文件管理器加上导航栏。M.patchFileManagerClass(infra/sui_patches.lua)先取状态、判断"本会话是否已 patch 过":
local layout_state = _acquireHooks(FileManager, FM_LAYOUT_STATE, plugin) local setup_already_patched = (layout_state == nil) local orig_setupLayout = layout_state and FileManager.setupLayout若已 patch 过(setup_already_patched为真),就直接跳过重新包装,仅刷新_live_plugin。这样即使文件管理器在一次会话中被重建 N 次,setupLayout上也始终只有一层SimpleUI 包装,不会叠出"导航栏套导航栏"。
⚠️ 注释里专门记录了一个真实事故:缺少这个守卫时,每次 FM 重建都会在外层再包一层,导致壁纸背景被新的白色容器盖住而"消失"。守卫把这种"随生命周期膨胀"的隐患彻底掐灭。
实战②:UIManager.close的会话级守卫
UIManager是全局单例,它的close被调用频率极高。M.patchUIManagerClose(infra/sui_patches.lua)采用会话级标志位防止叠加:
if UIManager._simpleui_close_patched then UIManager._simpleui_close_plugin = plugin -- 仅刷新指针 plugin._orig_uimanager_close = UIManager._simpleui_close_orig return end UIManager._simpleui_close_patched = true local orig_close = UIManager.close UIManager._simpleui_close_orig = orig_close plugin._orig_uimanager_close = orig_close UIManager.close = function(um_self, widget, ...) ... end再次进入时不再新建包装,只把"共享插槽"里的插件指针更新为最新实例。单个活着的包装永远通过.ui拿到当前文件管理器来做判断,从而在"关闭全屏组件 → 自动弹回主页屏"这条高频路径上保持轻量且正确。
📌 类似手法还用在
patchUIManagerShow(infra/sui_patches.lua)、壁纸注入patchWallpaperFM(infra/sui_patches.lua)等处,都是"标志位守卫 + 原函数存到类上"的组合拳。
可靠还原:teardownAll的逆序大卸载
安装入口M.installAll一口气装上二十多个 patch(infra/sui_patches.lua);与之对称的M.teardownAll(infra/sui_patches.lua)则负责逐一拆干净。它的顺序很有讲究:
- 壁纸钩子最后安装,所以最先释放(因为它叠在最上层);
- 接着恢复
UIManager.show/UIManager.close(调用频率最高、层级最深); - 然后依次还原
BookList.new、Menu、FileManager、readcollection等类方法; - 最后清理模块级状态(
_hs_boot_done、D-pad 焦点等),让"禁用→再启用"能从干净状态重启。
每个还原点都先查标志位再动手,例如:
if FM and FM._simpleui_deleteFile_patched and plugin._orig_fm_deleteFile then FM.deleteFile = plugin._orig_fm_deleteFile FM._simpleui_deleteFile_patched = nil plugin._orig_fm_deleteFile = nil end这套"标志位 + 原函数 + 逆序"三件套,确保插件无论被启用/禁用多少轮,最终 KOReader 都能回到与从未安装 SimpleUI 完全一致的初始状态。
小结
SimpleUI 的 monkey-patch 之所以"安全且可靠",靠的不是运气,而是把三件事制度化:
- 统一登记——
_acquireHooks/_addHook/_releaseHooks让每个包装都有据可查、可逆可拆; - 幂等守卫——标志位与
_acquireHooks的nil返回,杜绝"每开一本书叠一层"的膨胀陷阱; - 逆序还原 +
_live_plugin——teardownAll按安装的反序干净卸载,指针始终指向存活实例。
对任何想给 KOReader 写插件、或研究 Lua 动态修改类方法的同学,infra/sui_patches.lua 都堪称一份"生产级 monkey-patch 范本"。配合 main.lua 里的Patches.installAll/Patches.teardownAll调用时机,以及 modules/module_tbr.lua、screens/sui_bottombar.lua 等被钩子调用的具体模块,可以完整读懂"改进去"与"收回来"的全过程。
【免费下载链接】simpleui.koplugin
A highly customizable UI plugin for KOReader that features a home screen, bottom navigation bar, top bar and desktop modules/widgets.
相关推荐
ContextMenuManager与系统还原点:安全修改的保障
ContextMenuManager与系统还原点:安全修改的保障 你是否曾因误删注册表项导致右键菜单功能异常?是否担心优化右键菜单后系统出现不可预知的错误?本文
桌面应用系统工具revanced-patches安全性分析:确保修改应用的安全可靠
revanced patches安全性分析:确保修改应用的安全可靠 你是否担心使用修改版应用会带来安全风险?ReVanced Patches作为一个开源项目,通
移动开发快速上手Guake下拉终端:一条命令装好,5个技巧让你告别窗口来回切换
快速上手Guake下拉终端:一条命令装好,5个技巧让你告别窗口来回切换 在 GNOME 桌面上写代码的你,是不是也经历过这种崩溃瞬间:改一行日志,手忙脚乱地在编
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考