多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Monkey-Patch的艺术:SimpleUI如何安全修改KOReader类并可靠还原

Monkey-Patch的艺术:SimpleUI如何安全修改KOReader类并可靠还原 【免费下载链接】simpleui.kopluginA highly customizable UI plugin for KOReader that features a home screen, bottom navigation bar, top bar and desktop modules/widgets.项目地址https://gitcode.com/gh_mirrors/si/simpleui.koplugin点击查看免费下载SimpleUI 是一款面向KOReader的高度可定制 UI 插件它为电子书阅读器带来主页屏、底部导航栏、顶部状态栏和丰富的桌面模块。它的强大体验靠的正是infra/sui_patches.lua5673 行中一套精心的monkey-patch机制——在不改动 KOReader 源码的前提下劫持 并增强核心类的方法再在卸载时逐一可靠还原。本文带你精读这套钩子体系看懂一个插件如何做到改得进去、收得回来。为什么插件需要 Monkey-PatchKOReader 是开源阅读器插件无法直接修改它的源代码。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.luafor 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.patchFileManagerClassinfra/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.patchUIManagerCloseinfra/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拿到当前文件管理器来做判断从而在关闭全屏组件 → 自动弹回主页屏这条高频路径上保持轻量且正确。 类似手法还用在patchUIManagerShowinfra/sui_patches.lua、壁纸注入patchWallpaperFMinfra/sui_patches.lua等处都是标志位守卫 原函数存到类上的组合拳。可靠还原teardownAll的逆序大卸载安装入口M.installAll一口气装上二十多个 patchinfra/sui_patches.lua与之对称的M.teardownAllinfra/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.kopluginA highly customizable UI plugin for KOReader that features a home screen, bottom navigation bar, top bar and desktop modules/widgets.项目地址https://gitcode.com/gh_mirrors/si/simpleui.koplugin点击查看免费下载相关推荐ContextMenuManager与系统还原点安全修改的保障ContextMenuManager与系统还原点安全修改的保障 你是否曾因误删注册表项导致右键菜单功能异常是否担心优化右键菜单后系统出现不可预知的错误本文桌面应用系统工具revanced-patches安全性分析确保修改应用的安全可靠revanced patches安全性分析确保修改应用的安全可靠 你是否担心使用修改版应用会带来安全风险ReVanced Patches作为一个开源项目通移动开发快速上手Guake下拉终端一条命令装好5个技巧让你告别窗口来回切换快速上手Guake下拉终端一条命令装好5个技巧让你告别窗口来回切换 在 GNOME 桌面上写代码的你是不是也经历过这种崩溃瞬间改一行日志手忙脚乱地在编桌面应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表