通过应用程序域AppDomain加载和卸载程序集

微软装配车的大门似乎只为货物装载敞开大门,却将卸载工人拒之门外。车门的钥匙只有一把,若要获得还需要你费一些心思。我在学习Remoting的时候,就遇到一个扰人的问题,就是Remoting为远程对象仅提供Register的方法,如果你要注销时,只有另辟蹊径。细心的开发员,会发现Visual Studio.Net中的反射机制,同样面临这个问题。你可以找遍MSDN的所有文档,在Assembly类中,你永远只能看到Load方法,却无法寻觅到Unload的踪迹。难道我们装载了程序集后,就不能再将它卸载下来吗?

想一想这样一个场景。你通过反射动态加载了一个dll文件,如今你需要在未关闭程序的情况下,删除或覆盖该文件,那么结果会怎样?很遗憾,系统会提示你无法访问该文件。事实上该文件正处于被调用的状态,此时要对该文件进行修改,就会出现争用的情况。

显然,为程序集提供卸载功能是很有必要的,但为什么微软在其产品中不提供该功能呢?CLR 产品单元经理(Unit Manager) Jason Zander 在文章 Why isn't there an Assembly.Unload method? 中解释了没有实现该功能的原因。Flier_Lu在其博客里(Assembly.Unload)有详细的中文介绍。文中介绍了解决卸载程序集的折中方法。Eric Gunnerson在文章《AppDomain 和动态加载》中也提到:Assembly.Load() 通常运行良好,但程序集无法独立卸载(只有 AppDomain 可以卸载)。Enrico Sabbadin 在文章《Unload Assemblies From an Application Domain》也有相关VB.Net实现该功能的相关说明。

尤其是Flier_Lu的博客里已经有了很详细的代码。不过,这些代码没有详细地说明。我在我的项目中也需要这一项功能。这段代码给了我很大的提示。但在实际的实现中,还是遇到一些具体的问题。所以我还是想再谈谈我的体会。

通过AppDomain来实现程序集的卸载,这个思路是非常清晰的。由于在程序设计中,非特殊的需要,我们都是运行在同一个应用程序域中。由于程序集的卸载存在上述的缺陷,我们必须要关闭应用程序域,方可卸载已经装载的程序集。然而主程序域是不能关闭的,因此唯一的办法就是在主程序域中建立一个子程序域,通过它来专门实现程序集的装载。一旦要卸载这些程序集,就只需要卸载该子程序域就可以了,它并不影响主程序域的执行。

不过现在看来,最主要的问题不是子程序域如何创建,关键是我们必须实现一种机制,来达到两个程序域之间完成通讯的功能。如果大家熟悉Remoting,就会想到这个问题不是和Remoting的机制有几分相似之处吗?那么答案就可以呼之欲出了,对了,就是使用代理的方法!不过与Remoting不同的是两个程序域之间的关系。因为子程序域是在主程序域中建立的,因此对该域的控制显然就与Remoting不相同了。

我想先用一副图来表述实现的机制:

说明:
1、Loader类提供创建子程序域和卸载程序域的方法;
2、RemoteLoader类提供装载程序集方法;
3、Loader类获得RemoteLoader类的代理对象,并调用RemoteLoader类的方法;
4、RemoteLoader类的方法在子程序域中完成;
5、Loader类和RemoteLoader类均放在AssemblyLoader.dll程序集文件中;

我们再来看代码:
Loader类:

SetRemoteLoaderObject()方法:

  private AppDomain domain = null;
  private Hashtable domains = new Hashtable();  
  private RemoteLoader rl = null;
public RemoteLoader SetRemoteLoaderObject(string dllName)
{
    AppDomainSetup setup 
= new AppDomainSetup();            
    setup.ShadowCopyFiles 
= "true";
    domain 
= AppDomain.CreateDomain(dllName,null,setup);
            
    domains.Add(dllName,domain);    
    
try
    
{
                rl = (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap(
                "AssemblyLoader.dll","AssemblyLoader.RemoteLoader");         
    }

    
catch
    
{
        
throw new Exception();
    }

}


代码中的变量rl为RemoteLoader类对象,在Loader类中是其私有成员。SetRemoteLoaderObject()方法实际上提供了两个功能,一是创建了子程序域,第二则是获得了RemoteLoader类对象。

请大家一定要注意语句:
rl = (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll","AssemblyLoader.RemoteLoader");

这条语句就是实现两个程序域之间通讯的关键。因为Loader类是在主程序域中,RemoteLoader类则是在子程序域中。如果我们在Loader类即主程序域中显示实例化RemoteLoader类对象rl,此时调用rl的方法,实际上是在主程序域中调用的。因此,我们必须使用代理的方式,来获得rl对象,这就是CreateInstanceFromAndUnwrap方法的目的。其中参数一为要创建类对象的程序集文件名,参数二则是该类的类型名。

CreateCreateInstanceFromAndUnwrap方法有多个重载。代码中的调用方式是当RemoteLoader类为默认构造函数时的其中一种重载。如果RemoteLoader类的构造函数有参数,则方法应改为:

object[] parms = {dllName};
BindingFlags bindings 
= BindingFlags.CreateInstance |
BindingFlags.Instance 
| BindingFlags.Public;
rl 
= (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll","AssemblyLoader.RemoteLoader",true,bindings,
null,parms,null,null,null);

详细的调用方式可以参考MSDN。

以下Loader类的Unload方法和LoadAssembly方法():

public Assembly LoadAssembly(string dllName)
{
    
try
    
{
        SetRemoteLoaderObject(dllName);
        
return rl.LoadAssembly(dllName);
    }

    
catch (Exception)
    
{
        
throw new AssemblyLoadFailureException();
    }

}
public void Unload(string dllName)
{
    
if (domains.ContainsKey(dllName))
    
{
        AppDomain appDomain 
= (AppDomain)domains[dllName];
        AppDomain.Unload(appDomain);
        domains.Remove(dllName);
    }
            
}

当我们调用Unload方法时,则程序域domain加载的程序集也将随着而被卸载。LoadAssembly方法中的异常AssemblyLoadFailureException为自定义异常:

    public class AssemblyLoadFailureException:Exception
    
{
        
public AssemblyLoadFailureException():base()
        
{            
        }


        
public override string Message
        
{
            
get
            
{
                
return "Assembly Load Failure";
            }

        }


    }


既然在Loader类获得的RemoteLoader类实例必须通过代理的方式,因此该类对象必须支持被序列化。所以我们可以令该类派生MarshalByRefObject。RemoteLoader类的代码:

    public class RemoteLoader:MarshalByRefObject
    
{
        
public RemoteLoader(string dllName)
        
{
            
if (assembly == null)
            
{
                assembly 
= Assembly.LoadFrom(dllName);
            }

        }
        

        
private Assembly assembly = null;

        
public Assembly LoadAssembly(string dllName)
        
{
            
try
            
{
                assembly 
= Assembly.LoadFrom(dllName);                
                
return assembly;
            }

            
catch (Exception)
            
{
                
throw new AssemblyLoadFailureException();
            }

        }

    }


通过上述的两个类,我们就可以实现程序集的加载和卸载。另外,为了保证应用程序域的对象在内存中被清除,应该令这两个类都实现IDisposable接口,和实现Dispose()方法。

然而在实际的操作过程中,我发现在RemoteLoader类的LoadAssembly方法,是存在遗患的。在我的LoadAssembly方法中,会返回一个Assembly对象。令我百思不得其解的是,虽然都是Assembly对象,但在加载某些程序集并返回Assembly时,在Loader类中会抛出SerializationException异常,并报告反序列化的对象状态不足。这个异常是在序列化获反序列化过程中发生的。我反复比较了两个程序集,一个可以正常加载并序列化,一个会抛出如上异常。会抛出异常的程序集并没有什么特殊之处,且我在程序中的其他地方也没有重复加载该程序集。这是一个疑问!!

不过通常我们在RemoteLoader类中,要实现的方法并非返回一个Assembly对象,而是通过反射加载程序集后,创建该程序集的对象。由于类对象都为object类型,此时序列化就不会出现问题。在我的项目中,因为要获得程序集的版本号,比较版本号在确定是否需要更新,因此我在RemoteLoader类中,只需要在加载程序集后,返回程序集的版本号字符串类型就可以了。字符串类型是绝对支持序列化的。

AssemlbyLoader.Dll的源代码可以点击这里获得。在应用程序中,显示添加对该程序集的引用,然后实例化Loader类对象,来调用该方法即可。我还做了一个简单的测试程序,用的是LoadAssembly方法。大家可以测试一下,是否如我所说,对于某些程序集,可能会抛出序列化的异常!?

测试的代码请点击这里获得,测试界面如下:

同时,大家也可以测试一下,直接加载和通过AppDomain加载,删除程序集文件时会有什么区别?

posted @ 2004-09-29 15:21 张逸 阅读(11584) 评论(31) 编辑 收藏

有意思!
 回复 引用 查看   
#2楼 2004-09-30 08:39 吕震宇      
先收藏,慢慢读
 回复 引用 查看   
#3楼[楼主] 2004-10-03 18:42 wayfarer      
“然而在实际的操作过程中,我发现在RemoteLoader类的LoadAssembly方法,是存在遗患的。在我的LoadAssembly方法中,会返回一个Assembly对象。令我百思不得其解的是,虽然都是Assembly对象,但在加载某些程序集并返回Assembly时,在Loader类中会抛出SerializationException异常,并报告反序列化的对象状态不足。这个异常是在序列化获反序列化过程中发生的。我反复比较了两个程序集,一个可以正常加载并序列化,一个会抛出如上异常。会抛出异常的程序集并没有什么特殊之处,且我在程序中的其他地方也没有重复加载该程序集。这是一个疑问!!”

这是我的一个疑问,但Steve Maine给了我一个好的答案,我同意他的观点:

Check the dependencies of the assembly you're trying to load. If those dependancies are not in the GAC and not in the base directory of the running application, you'll get load failures. For some reason, the exception that gets thrown back across the AppDomain boundary triggers a SerializationException when it gets remoted.

 回复 引用   
#4楼 2004-11-04 15:25 Casablanca
为什么我使用你的例子加载你的dll是没有任何异常的,而加载我自己的就有异常?能帮我解决一下吗?
 回复 引用 查看   
#5楼[楼主] 2004-11-04 19:36 wayfarer      
Casablanca:我想知道的是你所说的异常是什么?异常信息是什么呢?

同时请注意你的依赖程序集是否正确!

如果你没有详细的错误信息,我是无能为力的。

 回复 引用   
#6楼 2004-11-08 15:47 Casablanca
Exception:反序列化对象的状态不足。需要详细信息。要想实现反序列化对象需要拥有什么样的属性?我已经实现了MarshalByRefObjecty呀
 回复 引用   
#7楼 2004-12-13 09:33 ytjia
为什么加载的程序集还是无法卸载呢?
 回复 引用 查看   
#8楼[楼主] 2004-12-13 14:48 wayfarer      
Casablanca:
你加上[Serializable]试一试。

@ytjia:
不知道你所说的是,没有卸载呢?还是无法正常卸载,抛出异常?
你可以下载我的源代码看一看。

 回复 引用   
#9楼 2004-12-14 09:12 ytjia
被加载的程序集文件还是被锁定的,我调用了Unload还是不能解除对被加载文件的锁定如下:
loader.Unload(textBox1.Text);
而且我跟踪Loader中的domain发现里面大部分属性值都是: AssemblyLoad <错误: 无法计算代理对象的字段> System.AssemblyLoadEventHandler

 回复 引用   
#10楼 2005-03-19 17:10 mouse
我也遇到这样的问题。但是我使用你的方法还是无法删除加载的程序集文件以下是我的代码:

Loader load = null;
if(File.Exists(Application.StartupPath + @"\AppConfig.dll"))
{
load = new Loader();
Assembly assembly = load.LoadAssembly(Application.StartupPath + @"\AppConfig.dll");
object o = assembly.CreateInstance("Config.AppConfig");
Type type = o.GetType();
if(type != null)
{
//代码略 }
o = null;
}
if(load != null)
{
load.Unload();
}

 回复 引用   
#11楼 2005-03-31 13:32 Apollo
这个方法不行!!我试了又试~~
在反射加载 dll 后 dll 没有给卸载掉啊

你在加载 dll 时
即使 dll 有 [Serializable] 也不一定能加载上

 回复 引用   
#12楼 2005-04-15 16:05 whb147
我用你的程序在加载dll的时候,加载不上,怎么回事?
提示:反序列化对象的状态不足。需要详细信息。
这是怎么回事?

 回复 引用   
#13楼 2005-06-06 11:54 sunday
load时,出现“凡序列化对象的状态不足。需要详细信息。”的原因之一,
有可能是,你的dll or exe 和要load的dll是否是在同一路径下。

尽供参考

 回复 引用   
#14楼 2005-07-27 15:41 天外来客[未注册用户]
先收藏,慢慢研究
 回复 引用   
#15楼 2005-07-28 13:19 tanjian[未注册用户]
做如下修改,便不会出现异常“反序列化对象的状态不足。需要详细信息。”加载失败的情况了,原因尚未深究,估计跟rl的创建有关。我的邮箱 tanjians@yahoo.com.cn 。欢迎大家一起探讨
public Assembly LoadAssembly(string dllName)
{
try
{
SetRemoteLoaderObject(dllName);
//return rl.LoadAssembly(dllName);
return Assembly.LoadFrom(dllName);;
}
catch
{
throw new AssemblyLoadFailureException();
}
}

 回复 引用   
#16楼 2005-08-01 11:39 meteor[未注册用户]
我用的方法和你的差不多.
不过另我头疼的是加载后删除不掉....
还是被程序锁定.


 回复 引用 查看   
#17楼 2005-10-19 08:43 随心所欲      
SerializationException的异常可能是你传递到proxy或者proxy的返回结果中不可以序列化的类或者变量

我的应用和你的差不多,也可以顺利实现这些功能,并且我加载的dll还不在一个文件夹下也能成功。
我的问题是,如何能让proxy中的实例调用DefaultDomain中的实例?

 回复 引用   
#18楼 2005-11-01 01:43 ego[未注册用户]
不行,用你的代码,打开我的DLL,卸载后还是被占用,删除不了 :(
 回复 引用   
#19楼 2005-11-01 01:48 ego[未注册用户]
唉,有些DLL是删除成功的,有些就不行,气死我了
 回复 引用 查看   
#20楼 2005-11-25 16:24 mahope      
“反序列化对象的状态不足。需要详细信息。”是因为RemoteLoader的方法public Assembly LoadAssembly(string dllName)返回的是Assembly ,这个Assembly 在反序列化的时候,可能需要附加什么东西,还没搞懂,研究中....
 回复 引用   
#21楼 2005-12-22 12:51 yanjiebing[未注册用户]
如果
1、加载的DLL与加载程序不在一个文件夹下;
2、加载的DLL本身有好多依赖项,且这些依赖项不在GAC中
那么
无论本地加载或是远程加载都会报错。

 回复 引用   
#22楼 2006-04-11 12:42 wangyp[未注册用户]
为什么我用两种方式加载dll文件,删除时都是错误的,说被占用了。急急急

 回复 引用   
#23楼 2006-05-31 14:25 ansign[未注册用户]
只要运行了:
assembly = Assembly.LoadFrom(dllName);
就绝对卸载不了。

 回复 引用   
#24楼 2006-12-24 13:25 enoch[未注册用户]
上面提到的方式我试验了一下,还是存在问题,一旦使用程序集合创建了对象,那该程序集还是不能被卸载;至于出现"反序列化对象的状态不足。需要详细信息。"异常的原因是因为在运行目录下找不到对应的应用程序集,无法加载指定的程序集才会出现的,只要把指定要加载的程序集所引用的程序集也拷到运行目录下面就可以了.
我记得前面的文章中有提到过使用程序集镜像可以解决程序集的"热插拔"问题!详细请链接http://zhuweisky.cnblogs.com/archive/2005/12/30/308218.html

 回复 引用   
#25楼 2007-01-15 11:10 ling[未注册用户]
为什么我下的源代码中没有类文件,查找不到namespace
CVST.AppFramework.AssemblyLoader,这个dll文件在bin目录下也找不到呀?

 回复 引用   
#26楼 2008-04-18 21:24 hzvincent[未注册用户]
请教一下,你的程序我试了一直报错,ex = {"无法将透明代理强制转换为类型“AssemblyLoader.RemoteLoader”。百思不得其解,期望指教

hz_vincent@sina.com

 回复 引用 查看   
#27楼 2008-11-26 10:17 劲草      
你好,问你一个问题,我的Dll 文件分了两个,一个是接口类,另一个是子类继承了接口,两个类不在同一个Dll文件里。
var DomainInstance = (TriggerDesignerFacadeBase)Domain.CreateInstanceFromAndUnwrap(FileName, "Workflow.TriggerDesignerFacade");

在创建域间通讯实例时,就抛出异常:System.TypeLoadException was caught
"Could not load type 'Workflow.TriggerDesignerFacade' from assembly 'Trigger_Time, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' because the parent type is an interface."

如何解决呢


 回复 引用   
#28楼 2009-03-05 15:10 yanlingevol[未注册用户]
你的程序有问题,不能返回assembly吧,一般不用返回assembly,直接通过loader对象操作该assembly即可,可以通过loader返回你想要的结果
 回复 引用 查看   
#29楼 2011-09-26 00:06 lexiaoyao20      
你好,为什么只有加载 CVST.AppFramework.AssemblyLoader.dll能够成功, 而加载 别的dll文件 就会 “ Assembly Load Failure"?
 回复 引用 查看   
#30楼 2012-01-07 09:01 凶狠的小白兔      
我测试了,的确是可用的,调试模式下是不可用的,要生成后再调用就可以了,具体原因自己想吧
Dll大全