无需重启资源管理器,如何更新Shell命名空间扩展?
我之前折腾过好多次Shell命名空间扩展的升级,太懂你想避免让用户重启一堆进程的痛点了!你提到的SHChangeNotify和SHFlushSFCache是对的方向,但光靠这俩还不够,得结合几个实操技巧才能真正实现无重启升级,下面是我踩过坑后总结的有效方案:
精准触发Shell的文件夹状态刷新
你用的SHCNE_ASSOCCHANGED其实是给文件关联变更用的,对命名空间扩展的针对性不强。试试组合这两个调用:// 通知系统刷新你的连接点文件夹本身 SHChangeNotify(SHCNE_UPDATEDIR, SHCNF_PATHW, L"C:\\YourJunctionFolderPath", NULL); // 触发文件夹重命名类的扩展事件,强制Shell重新读取desktop.ini SHChangeNotify(SHCNE_EXTENDED_EVENT, SHCNF_IDLIST, NULL, (LPVOID)SHCNEE_RENAMEFOLDER);这样能让已打开的资源管理器主动重新加载连接点的配置,而不是一直用缓存里的旧CLSID。
让系统主动卸载旧版DLL
除了SHFlushSFCache,一定要确保你的扩展DLL正确实现了DllCanUnloadNow方法——当没有活跃的COM对象时,必须返回S_OK。然后在新版本注册完成后,调用:CoFreeUnusedLibrariesEx(0, 0);这个函数会告诉系统释放所有当前未被使用的COM库,旧版DLL如果没有被进程占用,就会被卸载,这样新版本就能顺利加载了。
原子更新desktop.ini,避免读取异常
直接覆盖desktop.ini很容易出问题——比如资源管理器刚好在读取文件的时候,你半覆盖了,就会导致CLSID读取错误。正确的做法是:先写一个临时文件(比如desktop.ini.tmp),把新的CLSID和配置写进去,然后用原子操作替换掉旧的desktop.ini。替换完成后再调用上面的SHChangeNotify,确保Shell立刻感知到变化。用COM版本控制确保加载新版
注册扩展的时候,别只写CLSID的InprocServer32路径,最好给CLSID设置VersionIndependentProgID,或者用版本相关的ProgID。这样当你升级时,新版本的注册表项会覆盖旧的,系统在创建COM对象时会优先找最新的DLL路径。另外,ThreadingModel一定要设对(大部分Shell扩展用Apartment就好),不然可能会导致加载失败。定向刷新资源管理器进程
如果上面的方法还是有部分资源管理器没刷新,你可以枚举当前运行的资源管理器进程,给它们发刷新命令。比如用FindWindowEx找到资源管理器的窗口,发送WM_COMMAND消息触发刷新:HWND hExplorerWnd = FindWindowEx(NULL, NULL, L"CabinetWClass", NULL); if (hExplorerWnd) { SendMessage(hExplorerWnd, WM_COMMAND, ID_REFRESH, 0); }注意不同Windows版本的窗口类名可能有点不一样,比如Win10以前可能是
ExploreWClass,最好做个兼容判断。
这些方法组合起来,90%以上的场景都能实现无重启升级。当然如果有某个进程一直死死占用着旧版扩展的COM对象(比如某个程序一直在持续访问你的连接点文件夹),那可能还是得让用户重启那个进程,但这种情况很少见,大部分用户都能无感完成升级。
内容的提问来源于stack exchange,提问作者Jeremy Spiegel

