动态编译DLL无法卸载求助:AppDomain.Unload提示文件占用
解决脚本编译程序集无法卸载的问题
这个问题我之前也碰到过——.NET里单个程序集是没法在当前AppDomain里卸载的,你现在的代码直接在主域加载了编译后的程序集,还持有scriptAssembly的引用,同时创建了脚本实例,这些都会把文件死死锁住,导致AppDomain.Unload(其实你可能是想卸载程序集,但这个方法只能卸载整个域)报错。
问题根源
当你调用result.CompiledAssembly时,.NET会把编译好的程序集加载到当前应用程序域里,只要这个域里还有任何对该程序集的引用(比如Assembly对象、你创建的IScript实例),程序集文件就会被锁定,无法删除或覆盖,而且单个程序集根本没法在同一个域内卸载——这是.NET的设计限制,只能通过卸载整个AppDomain来释放程序集。
解决方案:用独立的AppDomain加载和运行脚本
核心思路是把脚本的加载、执行都放到一个单独的AppDomain里,执行完后直接卸载这个域,就能彻底释放程序集文件的锁。具体步骤如下:
1. 创建跨域执行的代理类
因为跨AppDomain访问对象需要遵循.NET的远程处理规则,所以我们需要一个继承自MarshalByRefObject的类来作为脚本执行的代理:
public class ScriptExecutor : MarshalByRefObject { public void RunScripts(string assemblyPath, Type interfaceType) { // 在新AppDomain里加载程序集 var scriptAssembly = Assembly.LoadFrom(assemblyPath); var scriptTypes = Helper.GetTypesImplementingInterface(scriptAssembly, interfaceType); // 注意:如果IScriptableComponent需要跨域访问,要么标记为[Serializable],要么也继承MarshalByRefObject IScriptableComponent component = new DummyComponent(); foreach (var scriptType in scriptTypes) { var script = (IScript)Activator.CreateInstance(scriptType); script.Run(component); } } }
2. 修改编译逻辑,避免当前域加载程序集
原来的CompileAssembly方法返回了CompiledAssembly,这会导致当前域加载程序集并锁定文件,我们改成只负责编译,不返回Assembly对象:
public static void CompileAssembly(string[] sourceFiles, string outputAssemblyPath) { var codeProvider = new CSharpCodeProvider(); var compilerParameters = new CompilerParameters { GenerateExecutable = false, GenerateInMemory = false, IncludeDebugInformation = false, OutputAssembly = outputAssemblyPath }; compilerParameters.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().Location); var result = codeProvider.CompileAssemblyFromFile(compilerParameters, sourceFiles); if (result.Errors.HasErrors) { var errorDetails = string.Join(Environment.NewLine, result.Errors.Cast<CompilerError>().Select(err => $"Line {err.Line}: {err.ErrorText}")); throw new Exception($"脚本编译失败:{Environment.NewLine}{errorDetails}"); } // 不要返回result.CompiledAssembly,避免当前域加载程序集 }
3. 修改按钮点击事件,使用独立AppDomain执行脚本
现在我们在主域里创建新的AppDomain,通过代理类执行脚本,执行完成后立即卸载域:
private void StartButton_Click(object sender, EventArgs e) { var compiledAssemblyPath = Path.Combine(Environment.CurrentDirectory, ScriptsDirectory, CompiledScriptsAssemblyName); var scriptFiles = Directory.EnumerateFiles(ScriptsDirectory, "*.cs", SearchOption.AllDirectories).ToArray(); // 先完成编译,不加载到当前域 Helper.CompileAssembly(scriptFiles, compiledAssemblyPath); // 配置新AppDomain的环境,确保能找到依赖 var domainSetup = new AppDomainSetup { ApplicationBase = Environment.CurrentDirectory, PrivateBinPath = ScriptsDirectory // 添加脚本目录作为私有路径,方便加载依赖 }; // 创建独立的AppDomain var scriptDomain = AppDomain.CreateDomain("ScriptExecutionDomain", null, domainSetup); try { // 在新域中创建代理实例并执行脚本 var executor = (ScriptExecutor)scriptDomain.CreateInstanceAndUnwrap( Assembly.GetExecutingAssembly().FullName, typeof(ScriptExecutor).FullName); executor.RunScripts(compiledAssemblyPath, typeof(IScript)); } finally { // 必须卸载AppDomain,释放程序集文件锁 AppDomain.Unload(scriptDomain); } }
注意事项
- 跨域对象的要求:如果你的
IScript、IScriptableComponent或者DummyComponent需要在主域和脚本域之间传递,要么标记为[Serializable](会被序列化复制),要么继承MarshalByRefObject(会创建远程代理,实际对象留在原域)。如果不满足这个条件,跨域调用会报错。 - 临时文件处理:如果不需要保留编译后的程序集文件,可以把
GenerateInMemory设为true,这样编译出来的程序集会直接加载到内存,不会生成物理文件,但同样需要用独立AppDomain来卸载。 - 异常处理:建议在脚本执行的逻辑里添加异常捕获,避免脚本的错误导致主域崩溃。
内容的提问来源于stack exchange,提问作者user8429238
相关产品推荐
相关产品推荐

