• 通过应用程序域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加载,删除程序集文件时会有什么区别?

  • 相关阅读:
    C#操作REDIS例子
    A C# Framework for Interprocess Synchronization and Communication
    UTF8 GBK UTF8 GB2312 之间的区别和关系
    开源项目选型问题
    Mysql命令大全——入门经典
    RAM, SDRAM ,ROM, NAND FLASH, NOR FLASH 详解(引用)
    zabbix邮件报警通过脚本来发送邮件
    centos启动提示unexpected inconsistency RUN fsck MANUALLY
    rm 或者ls 报Argument list too long
    初遇Citymaker (六)
  • 原文地址:https://www.cnblogs.com/wayfarer/p/47896.html
Copyright © 2020-2023  润新知