我在C#(ApplicationClass)中使用Excel互操作,并在finally子句中放置了以下代码:
while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();
尽管这种方法有效,但即使在我关闭Excel之后,Excel.exe进程仍处于后台。它只在我的应用程序被手动关闭后发布。
我做错了什么,或者是否有其他方法可以确保正确处理互操作对象?
我不敢相信这个问题已经困扰了世界五年。。。。如果您已经创建了应用程序,则需要在删除链接之前先关闭它。
objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
关闭时
objBook.Close(true, Type.Missing, Type.Missing);
objExcel.Application.Quit();
objExcel.Quit();
当您新建一个excel应用程序时,它会在后台打开一个excel程序。您需要在释放链接之前命令该excel程序退出,因为该excel程序不是您直接控制的一部分。因此,如果链接被释放,它将保持开放!
每个人都很好编程~~
“千万不要在COM对象中使用两个点”是避免COM引用泄漏的一条很好的经验法则,但Excel PIA会导致泄漏的方式比乍一看更明显。
其中一种方法是订阅任何Excel对象模型的COM对象公开的任何事件。
例如,订阅Application类的WorkbookOpen事件。
关于COM事件的一些理论
COM类通过回调接口公开一组事件。为了订阅事件,客户端代码可以简单地注册实现回调接口的对象,COM类将调用其方法以响应特定事件。由于回调接口是一个COM接口,因此实现对象的职责是减少它为任何事件处理程序接收的任何COM对象(作为参数)的引用计数。
Excel PIA如何公开COM事件
Excel PIA将Excel应用程序类的COM事件公开为常规的.NET事件。每当客户端代码订阅.NET事件(强调“a”)时,PIA都会创建实现回调接口的类的实例,并将其注册到Excel中。
因此,为了响应来自.NET代码的不同订阅请求,许多回调对象被注册到Excel中。每个事件订阅一个回调对象。
用于事件处理的回调接口意味着,PIA必须为每个.NET事件订阅请求订阅所有接口事件。它不能挑挑拣拣。在接收到事件回调时,回调对象检查关联的.NET事件处理程序是否对当前事件感兴趣,然后调用该处理程序或无提示地忽略回调。
对COM实例引用计数的影响
所有这些回调对象都不会减少它们接收的任何COM对象(作为参数)对任何回调方法的引用计数(即使是被忽略的回调方法)。它们只依赖CLR垃圾收集器来释放COM对象。
由于GC运行是非确定性的,这可能导致Excel进程延迟的时间比预期的长,并造成“内存泄漏”的印象。
解决方案
目前唯一的解决方案是避免PIA的COM类事件提供程序,并编写自己的事件提供程序来确定是否释放COM对象。
对于Application类,这可以通过实现AppEvents接口,然后使用IConnectionPointContainer接口将实现注册到Excel来完成。Application类(以及使用回调机制公开事件的所有COM对象)实现IConnectionPointContainer接口。
这似乎太复杂了。根据我的经验,要让Excel正确关闭,只有三件关键的事情:
1:确保没有对您创建的excel应用程序的剩余引用(无论如何,您应该只有一个引用;将其设置为空)
2:调用GC.Collect()
3:必须通过用户手动关闭程序或通过对Excel对象调用“退出”来关闭Excel。(请注意,“退出”的功能与用户试图关闭程序的功能相同,如果存在未保存的更改,即使Excel不可见,也会显示一个确认对话框。用户可以按“取消”,Excel将不会关闭。)
1需要在2之前发生,但3可以随时发生。
实现这一点的一种方法是用自己的类包装interop Excel对象,在构造函数中创建interop实例,并使用Dispose实现IDisposable
if (!mDisposed) {
mExcel = null;
GC.Collect();
mDisposed = true;
}
这将从程序的方面清理出优秀的东西。一旦Excel关闭(由用户手动或您调用退出),该过程将消失。如果程序已经关闭,那么进程将在GC.Collect()调用中消失。
(我不确定它有多重要,但您可能需要在GC.Collect()调用之后调用GC.WaitForPendingFinalizers(),但这并不是完全需要摆脱Excel进程。)
多年来,这对我来说毫无问题。请记住,虽然这是有效的,但实际上您必须优雅地关闭它才能工作。如果在清理excel之前中断程序(通常在调试程序时单击“停止”),则仍会累积excel.exe进程。
正如其他人所指出的,您需要为使用的每个Excel对象创建一个显式引用,并对该引用调用Marshal.ReleaseComObject,如本知识库文章所述。您还需要使用try/finally来确保始终调用ReleaseComObject,即使抛出异常也是如此。即,代替:
Worksheet sheet = excelApp.Worksheets(1)
... do something with sheet
你需要做一些事情,比如:
Worksheets sheets = null;
Worksheet sheet = null
try
{
sheets = excelApp.Worksheets;
sheet = sheets(1);
...
}
finally
{
if (sheets != null) Marshal.ReleaseComObject(sheets);
if (sheet != null) Marshal.ReleaseComObject(sheet);
}
如果要关闭Excel,还需要在释放Application对象之前调用Application.Quit。
正如您所看到的,只要您尝试做任何稍微复杂的事情,这很快就会变得非常笨拙。我用一个简单的包装类成功地开发了.NET应用程序,该类包装了Excel对象模型的一些简单操作(打开工作簿、写入范围、保存/关闭工作簿等)。包装器类实现IDisposable,在它使用的每个对象上仔细地实现Marshal.ReleaseComObject,并且不向应用程序的其他部分公开任何Excel对象。
但这种方法不能很好地适应更复杂的需求。
这是.NETCOM互操作的一大缺陷。对于更复杂的场景,我会认真考虑用VB6或其他非托管语言编写ActiveX DLL,您可以将与进程外COM对象(如Office)的所有交互委托给它。然后,您可以从.NET应用程序中引用此ActiveX DLL,因为您只需要发布这一个引用,所以事情会变得更加简单。