如何在Silverlight 4中释放COM对象

6
当使用COM互操作与Office(通常是Excel)时,我总是仔细确保在每个引用上调用Marshal.ReleaseComObject,以避免Excel不退出的问题,如此KB文章中所述
当我从OOB Silverlight应用程序(使用AutomationFactory.CreateObject)使用Interop时,如何确保Excel退出?
Silverlight没有Marshal.ReleaseComObject方法,即使调用GC.CollectGC.WaitForPendingFinalizers也无济于事。
毫无疑问,Microsoft没有添加此功能到Silverlight而没有释放COM引用的机制?对于自动化外部进程的COM服务器(例如Excel),这对我来说似乎是一个停滞不前的问题。
这是一个令人惊讶的省略,尤其是Pete Brown在他的书“Silverlight 4 in Action”的第5.5节中甚至如此说:AutomationFactory.CreateObject

此功能的主要目的是允许自动化其他应用程序,包括Microsoft Office。

更新:作为对Hans评论的回应。
我不确定“无声刺客”问题是否存在于Office应用程序的典型自动化中。常见用法可能看起来像以下内容,我在WinForms应用程序中反复看到过这种用法,从未遇到过Hans链接的文章中描述的“毒害RCW”:
  • 创建一个Excel.Application实例
  • 打开或创建工作簿
  • 将数据写入工作簿
  • 如果一切顺利,则显示Excel,关闭工作簿并调用Application.Quit。
  • 调用Marshal.ReleaseComObject以释放所有Excel对象引用。
不按照Hans建议的那样调用Marshal.ReleaseComObject将留下多个Excel.exe副本运行,如上述提到的KB文章所述-非常不希望。
更新2:
我用于重现此问题的示例是Pete Brown的书“Silverlight 4 in Action”的源代码示例,此页面上有下载链接。示例解决方案AutomatingExcel在Ch05.zip / 5.03中。要重现:
  • 确保没有Excel实例正在运行
  • 运行AutomatingExcel示例
  • 打开了一个Excel工作簿
  • 关闭Excel
  • 使用任务管理器观察Excel仍在运行。
将所有动态变量设置为null并调用GC.Collect()似乎像AnthonyWJones的答案中指出的那样起作用。

Otaku的回答是我所需要的——通过在using语句中包装引用,COM引用将被释放,无需调用GC.Collect。经过一些实验表明,与上面引用的KB文章中描述的标准的Marshal.ReleaseComObject解决方案相比,它更容易处理未能正确处置每个引用的情况。

有一个权威的看法,确切地说明必须处置哪些内容才能确保所有Excel引用都被释放,这将是很有趣的。


可能是When to use RelaseComObject vs FinalReleaseComObject?的重复问题。 - Hans Passant
@Hans:Silverlight的Marshal类中没有这两种方法。 - AnthonyWJones
@Hans,你为什么认为终结器线程“没有运行”?我不是在谈论特定的应用程序,而是试图了解当将当前工作代码迁移到Silverlight时,我将如何解决这个问题。 - Joe
@Hans “因为终结器线程释放了RCWs” - 但是时间不可预测?因此出现了KB文章中描述的问题。 - Joe
为什么它必须是可预测的?这在设计中是隐含的,它自己决定,并不太关心你是否停止使用Excel。你已经知道如何使它可预测了。 - Hans Passant
显示剩余4条评论
4个回答

2
你可以实现 IDisposable 接口。我见过的最好的例子是在 http://csfun.blog49.fc2.com/blog-entry-79.html 上。这篇博客文章是日语的,但如果你不会日语,可以在 Chrome 中打开,并让 Google 为你翻译页面。

另外,如果你只想直接获取 COM 包装器的源代码示例,你可以下载它所在的示例应用程序:SilverOffice


+1 谢谢 - 这就是我在寻找的答案 - 如何在不调用GC.Collect的情况下释放COM对象。看起来,使用dynamic关键字创建的引用实现了IDisposable,并且这会释放COM引用。遗憾的是MSDN在这个主题上没有提供更明确的信息:http://msdn.microsoft.com/en-us/library/dd264733.aspx - Joe
这非常有趣,我没有考虑过测试和使用 IDisposable,不幸的是,释放并不能释放 COM 对象。 - AnthonyWJones
@Joe:我刚刚通过在我的代码中添加Dispose来进行了测试。没有GC.Collect,我仍然会得到多个Excel实例。 - AnthonyWJones
@AnthonyWJones:你试过参考示例项目SilverOffice了吗?代码中有一个InstanceManager类,它确保只使用(和处理)一个Excel实例。 - Todd Main
不,我没有。我只是想看看是否使用原始的IDisposable解决了这个问题。当然,对于大量的办公工作,使用实例管理器以及大量其他包装器工作是非常有意义的。然而,根本问题似乎仍然存在;无论是否调用Dispose,COM对象似乎只有在最终化时才会被实际释放。 - AnthonyWJones

1

看一下这段代码:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        dynamic app = AutomationFactory.CreateObject("Excel.Application");
        dynamic book = app.Workbooks.Add();
        dynamic sheet = app.ActiveSheet();

        sheet = null;
        book.Close(false);
        book = null;
        app.Quit();
        app = null;

        GC.Collect();
    }

Excel进程出现并立即消失。移除GC,Excel进程将继续运行。如果您逐字复制此代码是否会得到相同的结果?如果是,则表明在代码中某处仍然可以从一个线程堆栈或静态字段访问Excel对象。

您是否曾将Excel对象保存在字段(而不是局部变量)中?

您是否将Excel对象保存在看似变量但被动态委托或Lambda引用的变量中,并将其用作事件处理程序?

您是否正在将事件处理程序附加到具有短寿命对象的长寿命对象?如果是,请确保正确分离这些处理程序。

这些问题中的许多因素可能会使开发人员认为他们已经准备好进行GC的对象,但GC发现它们仍然可以访问,因此不能被收集。

如果上述代码不能产生相同的行为,则我们正在寻找完全不同的问题。 我正在使用最新的SL 4运行时在Server 2008 R2上使用Office 2007。但是,如果我们由于设置不同而存在差异,那么我们就处于非常不稳定的地位。

编辑

经过一些测试,这似乎是有效的:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        using (dynamic app = AutomationFactory.CreateObject("Excel.Application"))
        {
            using (dynamic book = app.Workbooks.Add())
            {
                using (dynamic sheet = app.ActiveSheet())
                {

                }
                book.Close();
            }
            app.Quit();
        };

        GC.Collect();
    }

然而,如果不关闭GC,最终会留下不需要的Excel进程。


看起来很有前途。我的目标环境是Excel 2003,但我已经在家里尝试了您的代码在Excel 2007上运行,并且它可以工作。将变量设置为null似乎是必需的,不确定为什么,因为它们应该是无法访问的。然而,GC.Collect()似乎是释放COM引用的重型武器。 - Joe
@Joe:不一定,如果你从我的代码中删除 x=null,那么在调用 GC.Collect 的时候,你可能会看到 Excel 进程仍然在运行,尽管已经调用了 GC.Collect。这是因为当 Collect 被调用时,所有线程都被暂停,GC 将检查它们的堆栈,以查找由局部变量持有的引用。我使用“可能”这个词,因为优化可能使它们无法访问。如果你将大部分代码移到另一个函数中,并从 Click 事件调用它,那么在赋值为 null 后调用 GC.Collect 就不再需要了。 - AnthonyWJones
+1并感谢您的持续调查。您的代码可能存在隐式实例化(“双点”问题)。您是否尝试过“using(dynamic workbooks = app.Workbooks)”后跟“workbooks.Add()”来确保所有引用都被明确实例化和处理。在标准COM互操作中,仔细执行此操作是必要的,而未能这样做可能会解释为什么最终会运行进程。 - Joe
@Joe:不错!确保通过“Workbooks”返回的对象也被处理掉确实解决了问题。不需要使用GC.Collect。 - AnthonyWJones

0
通过在using语句中包装引用,COM引用将被释放。
请注意:using语句只是try/catch/finally的语法糖,其中finally子句中包含Dispose()。
此外,大多数应用程序在这种情况下不允许使用using语句,因为创建和清理COM对象分布在各个地方/方法中。
这里需要的关键行是:
((IDisposable)_excel).Dispose();  // Release COM Interface

这个假设:

dynamic _excel = AutomationFactory.CreateObject("Excel.Application");

0

我会考虑在WCF服务中构建Excel文件。您可以在那里进行所有的清理工作。将字节发送到Silverlight应用程序,然后使用SaveFileDialog将它们发送给用户。

Silverlight对访问客户端文件系统有限制。在客户端系统上操作Excel文件,甚至假设客户端已安装Excel似乎违反了这一限制。


虽然你的方法是正确的,但它并没有回答问题。而且,在具有提升信任 OOB 应用程序的客户端自动化 Excel 方面可能存在合法的原因。 - Joe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接