如何强制对象在由新的 AppDomain 和 ConstructorInfo.Enabled 创建时释放引用?



下面是另一个释放反射创建的对象的方法:

我们正在使用一个报告工具(Active Reports 6),它为每个报告创建一个dll。

我们有很多客户使用类似但仍然独特的报告。

通过web界面读取报告。我们运行多个站点,每个客户端一个。

我们的选择是:1) 把所有的报告放在一个大项目中,所有的网站都会调用这个项目。成本:每次我们对任何一个报告进行小的更改时,都需要重新编译它,这可能会给所有网站带来问题。2) 创建一大堆类似的小项目,每个网站都有一个——为了空间的考虑,这也会带来问题。3) 创建一个"报表工厂",它将根据需要使用反射来连接报表dll。

我们选择了"3"。

问题:最终产品运行良好,但有一点除外:完成后不会发布报表dll。测试环境中的操作当前没有问题,但如果您尝试使用报表dll在文件夹中执行任何操作,则会收到以下错误消息:"此操作无法完成,因为文件夹或其中的文件在另一个程序中打开"

在研究了这个网站和其他网站后,我们意识到我们需要为每个调用提供一个可以干净卸载的AppDomain。在仍然存在问题之后,我们意识到AppDomainSetup对象需要有一个允许它为多个用户进行优化的设置(LoaderOptimization.MultiDomain)那没用。

不幸的是,基本对象(Active6报告)无法序列化,因此我们无法进行深度复制并丢弃原始对象。

在做了所有这些之后,我们仍然遇到了问题。

这是代码(C#):

private object WireUpReport(ReportArgs args)
{
    //The parameter 'args' is a custom type (ReportArgs) which merely contains a 
name/value pair collection.

    object myReport = null;
    string sPath = String.Empty;
    string sFriendlyName = String.Empty;
    sFriendlyName = System.Guid.NewGuid().ToString();
    Assembly asmReport = null;
    AppDomainSetup ads = null;
    AppDomain adWireUp = null;
    ConstructorInfo ci = null;
    Type myReportType = null;
    Type[] parametypes = null;
    object[] paramarray = null;
    object retObject = null;
    try
    {
        //Get Report Object
        sPath = GetWireUpPath(args); //Gets the path to the required dll; kept in a config file
                                     //This parameter is used in an overloaded constructor further down
        ads = new AppDomainSetup();
        ads.ApplicationBase = Path.GetDirectoryName(sPath);
        ads.LoaderOptimization = LoaderOptimization.MultiDomain;
        adWireUp = AppDomain.CreateDomain(sFriendlyName, AppDomain.CurrentDomain.Evidence, ads);
        asmReport = adWireUp.GetAssemblies()[0];
        asmReport = Assembly.LoadFrom(sPath);
        //Create parameters for wireup
        myReportType = asmReport.GetExportedTypes()[0];
        parametypes = new Type[1];
        parametypes[0] = typeof(ReportArgs);
        ci = myReportType.GetConstructor(parametypes);
        paramarray = new object[1];
        paramarray[0] = args;
        //Instantiate object
        myReport = ci.Invoke(paramarray);
        return myReport;
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        //Make sure Assembly object is released.
        if (adWireUp != null)
        {
            AppDomain.Unload(adWireUp);
        }
        if (asmReport != null)
        {
            asmReport = null;
        }
        if (ads != null)
        {
            ads = null;
        }
        if (adWireUp != null)
        {
            adWireUp = null;
        }
        if (ci != null)
        {
            ci = null;
        }
        if (myReportType != null)
        {
            myReportType = null;
        }
        if (parametypes != null)
        {
            parametypes = null;
        }
        if (paramarray != null)
        {
            paramarray = null;
        }
    }
}

从该代码返回的对象被强制转换为ActiveReports类型,然后在我们的应用程序中传递。

如有任何帮助,我们将不胜感激。感谢

您的代码似乎严重误解了如何与单独的AppDomain交互。

想象一下与AppDomain的交流,就像与目前在另一个国家的人交谈一样。你知道他们在哪里,但你不能只是走过去和他们说话。如果你想让他们为你做点什么,你必须打开沟通渠道,告诉他们你需要什么。

打开通信线路的方法是定义一个代理对象,该代理对象可以在另一个AppDomain中创建,然后跨越边界返回到当前的AppDomain。要想跨越边界,需要将对象标记为[Serializable]或从MarshalByRefObject继承。因为我们实际上想与另一个AppDomain中的引用对话,而不仅仅是拥有它的副本,所以我们需要代理来完成后者。

private class CrossDomainQuery : MarshalByRefObject
{
    public void LoadDataFromAssembly(string assemblyPath)
    {
        var assembly = Assembly.LoadFrom(assemblyPath);
        //TODO: Do something with your assembly
    }
}

AppDomain上有一个名为CreateInstanceAndUnwrap()的方法,它将在另一个AppDomain中创建该通信对象的实例,然后将可以强制转换为代理类型的__TransparentProxy对象交还给您。

var crossDomainQuery = (CrossDomainQuery)adWireUp.CreateInstanceAndUnwrap(
    typeof(CrossDomainQuery).Assembly.FullName,
    typeof(CrossDomainQuery).FullName);

一旦你有了代理对象,你就可以调用它上的方法,它们将在另一个AppDomain中被调用

crossDomainQuery.LoadDataFromAssembly(assemblyPath);

那么这与您当前的示例代码有什么不同呢

您当前的代码实际上并没有在其他AppDomain中执行任何有用的东西

adWireUp = AppDomain.CreateDomain(sFriendlyName, AppDomain.CurrentDomain.Evidence, ads);
asmReport = adWireUp.GetAssemblies()[0];
asmReport = Assembly.LoadFrom(sPath);

这会创建一个新的AppDomain,但随后会将该AppDomain中的所有程序集加载到当前的AppDomain中此外,它还显式地将报表程序集加载到当前的AppDomain中。

创建一个AppDomain并在上面调用方法并不意味着你的代码在里面执行,就像阅读关于另一个国家的信息意味着你现在正在和里面的人交谈一样

即使您确实创建了一个代理对象并在另一个AppDomain中执行代码,也需要注意一些事情。

1) 两个AppDomain都必须能够看到用于代理的类型,您可能必须手动(至少临时)处理其中一个AppDomain的AssemblyResolve事件以帮助解决此问题。

2) AppDomain的创建成本相当高。一般来说,它们不用于需要快速旋转、采取一些行动然后消失的情况。你应该计划尽可能长时间地让他们在身边,或者准备好在每次通话中承受性能打击。

3) 您已经说过,您正在实例化的报表类型是不可序列化的,并且能够序列化对象是将该类型从其他AppDomain传递回的一个要求。定义一个可以跨边界传输相关数据的可序列化类并使用它来传递报告数据可能是一种选择,但您必须确定这是否适用于您的特定情况。

此外,顺便说一句,除非您有依赖于变量设置为null的逻辑,否则在您的finally中将所有内容设置为null不会有任何用处,还会使代码复杂化。

最新更新