
做插件系统时,几乎每个人都会先想到一个方案:
"插件更新了,我在程序运行中把文件换掉,再让主程序重新加载一次。"
听起来很自然,实现起来会撞上三个硬约束。它们不是某个平台的限制,而是模块加载这套机制本身带来的。
先说明一点:这三条都不是“文件缺失”类问题,所以用户侧的 DLL修复工具 在这里帮不上忙——文件一个不少,版本与签名都可能正常,出问题的是内存里的状态与类型关系。
已经加载的模块会被系统持有引用,删除或覆盖会直接失败。
常见的绕过办法是改名:句柄指向的是文件本身,而不只是文件名,所以改名不会影响已加载的模块。于是新文件可以放进去。
但要注意这里发生了什么:
也就是说,"替换成功"只是完成了第一步。要让新的那份真正生效,必须让旧模块从进程里卸载掉——这就引出了后两条约束。
即使你成功卸载了模块,它在运行时留下的东西不一定随之清理:
尤其麻烦的是:某些运行时环境本身并不保证卸载后资源完全释放。
表现出来就是那个熟悉的现象:反复加载与卸载同一个模块,内存和句柄会缓慢增长——每次泄漏一点,跑得越久越明显。
所以"热重载"在长驻进程里往往是慢性问题,而不是立刻显形的问题。
前两条还能勉强绕,这一条很难绕。
同一个类,在两个"不同次加载"的模块里,对运行时来说是两个不同的类型。
这不是哲学问题,而是有直接后果的:
这条约束解释了为什么"热重载"在带面向对象接口的插件系统里尤其难做:对象一旦跨过模块边界,"同一个类"这个前提就不成立了。
这张表传达的核心判断是:能低成本重载的是"数据与脚本",二进制模块的重载代价很高。
这是一个设计取舍,不是技术缺陷。 成熟方案里大量采用第三种做法,正是因为它把"可替换"的部分限制在了不会引发类型问题的范围内。
第一,把状态与逻辑分离。
状态集中存放并且可序列化,逻辑(模块)可替换。这样"重载"就变成了"重新装载逻辑 %2B 恢复状态"。
第二,只在没有活跃引用时切换。
引用计数归零才是安全的替换时机。有对象在用时替换,等于主动制造类型问题。示意:
// reload only when no live references remain
if (plugin.use_count() == 0) {
plugin.reset(); // unload the current build
plugin.load(newPath); // load the new one
}第三,接受少量泄漏,把它变成可控成本。
限定重载次数,或者定期安排一次重启。很多成熟方案就是这么做的——与其追求"永不重启",不如把重启做成计划内的动作。
第四,明确接口边界。
不跨模块传递复杂对象,用结构化的数据跨越边界。这一条同时解决了类型身份问题与生命周期管理问题。
三个特征,凑齐两条就值得怀疑:
处理方向不是继续加固热重载,而是把方案换成"重启进程"或"进程分离"。
顺带说明一件事:这类问题与文件完整性无关——文件都在、版本也对,甚至签名都正常。所以像 DLL修复工具 这类“扫描并补齐缺失文件”的工具在这里没有任何用武之地,它不解决状态与类型层面的问题。
关于"运行中替换模块",记住三条:
所以工程上的结论很朴素:把可重载的部分做成数据与脚本,把二进制模块的重载换成计划内的重启。 与其在这三条约束上硬碰,不如承认它们,然后按它们来设计——毕竟 DLL修复工具 能补的是文件,补不了类型身份。
https://www.ijinshan.com/functions/repairdll.html?channel=4073
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。