如何正确清理Excel互操作对象?

805

我正在使用C#中的Excel互操作 (ApplicationClass),并已将以下代码放置在我的finally子句中:

while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();
尽管这样做是有效的,但即使在关闭Excel后,Excel.exe进程仍然在后台运行。只有手动关闭应用程序后,它才会释放。我做错了什么,或者有没有其他方法确保interop对象被正确处理?

3
你是想在不关闭应用程序的情况下关闭Excel.exe吗?我不确定我完全理解你的问题。 - Bryant
4
我正在尝试确保未受控制的互操作对象能够得到正确的处理和释放。这样即使用户已经完成了从应用程序创建的Excel电子表格,也不会出现Excel进程挂起的情况。 - HAdes
4
如果你能尝试通过生成XML Excel文件来完成它,否则请考虑使用VSTO托管/非托管内存管理:http://jake.ginnivan.net/vsto-com-interop - Jeremy Thompson
这个能很好地转换成Excel吗? - Paul C
但是我仍然有两个建议针对上面的代码:1)你应该使用Marshal.FinalReleaseComObject(excelSheet)而不是使用这个while循环2)当“excelSheet”是一个局部变量时,行“excelSheet = null;”是不需要的 - jreichert
2
请参考微软的支持文章,其中他们专门提供了解决此问题的方案:http://support.microsoft.com/kb/317109/ - Arjan
43个回答

4

正如其他人所指出的那样,您需要为使用的每个Excel对象创建一个明确的引用,并调用Marshal.ReleaseComObject释放该引用,如此KB文章所述。 您还需要使用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。

当你尝试进行稍微复杂的操作时,你会发现这很快变得非常难以处理。我已经成功地开发了一个简单的包装类来包装一些Excel对象模型的简单操作(打开工作簿,写入范围,保存/关闭工作簿等)。这个包装类实现了IDisposable接口,在使用每个对象时小心地实现Marshal.ReleaseComObject,并且不向应用程序的其余部分公开任何Excel对象。

但是,这种方法对于更复杂的需求并不适用。

这是.NET COM互操作的一个重大缺陷。对于更复杂的情况,我会认真考虑编写一个ActiveX DLL,使用VB6或其他非托管语言将所有与外部COM对象(如Office)的交互委派给它。然后,您可以从.NET应用程序引用此ActiveX DLL,这样事情就会变得更加容易,因为您只需要释放这一个引用。


3
当以上所有方法都无效时,请尝试给Excel留一些时间来关闭其工作表:
app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();

...
FinalReleaseComObject(app);

3
我讨厌任意的等待。但是因为你关于需要等待工作簿关闭的想法是正确的,所以+1。另一种方法是在循环中轮询工作簿集合,并将任意等待作为循环超时。 - dFlat
我非常怀疑Close()方法不是阻塞调用,也就是说,我认为在工作簿实际关闭之前,执行不会继续。如果Close()方法返回并且您可以枚举工作簿集合并且它不为空,则在我看来这将是一个严重的错误。我宁愿循环遍历工作簿并调用.Close(false),然后再调用Quit() - Alexander Høst

3

确保释放与Excel相关的所有对象!

我尝试了几种方法,都是很好的想法,但最终发现我的错误:如果您没有释放所有对象,则以上任何方法都无法帮助您 就像在我的情况下一样。确保您释放包括范围对象在内的所有对象!

Excel.Range rng = (Excel.Range)worksheet.Cells[1, 1];
worksheet.Paste(rng, false);
releaseObject(rng);

选项都在这里

非常好的观点!我认为因为我使用的是Office 2007,所以有一些清理工作是由它来完成的。我已经使用了上面的建议,但是没有像你在这里建议的那样存储变量,EXCEL.EXE确实退出了,但我可能只是幸运而已,如果我有任何进一步的问题,我一定会看看我的代码中的这部分内容 =) - Paul C

2

在使用Word/Excel互操作应用程序时一定要非常小心。尝试了所有的解决方案后,我们仍然有很多“WinWord”进程在服务器上保持打开状态(超过2000个用户)。

经过数小时的研究,我发现如果同时在不同的线程上使用Word.ApplicationClass.Document.Open()打开超过两个文档,IIS工作进程(w3wp.exe)就会崩溃,导致所有的WinWord进程都无法关闭!

因此,我想这个问题没有绝对的解决方案,但可以考虑转换到其他方法,例如Office Open XML开发。


2
这里有一个非常简单的方法来做到这一点:

[DllImport("User32.dll")]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
...

int objExcelProcessId = 0;

Excel.Application objExcel = new Excel.Application();

GetWindowThreadProcessId(new IntPtr(objExcel.Hwnd), out objExcelProcessId);

Process.GetProcessById(objExcelProcessId).Kill();

2
我的解决方案
[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);

private void GenerateExcel()
{
    var excel = new Microsoft.Office.Interop.Excel.Application();
    int id;
    // Find the Excel Process Id (ath the end, you kill him
    GetWindowThreadProcessId(excel.Hwnd, out id);
    Process excelProcess = Process.GetProcessById(id);

try
{
    // Your code
}
finally
{
    excel.Quit();

    // Kill him !
    excelProcess.Kill();
}

2
两个点的规则对我没有用。在我的情况下,我创建了一个方法来清理我的资源,如下所示:
private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}

1

我目前正在进行办公自动化的工作,并找到了一个解决方案,它对我来说每次都有效。它很简单,不涉及杀死任何进程。

似乎只需通过循环遍历当前活动进程,并以任何方式“访问”打开的Excel进程,即可删除任何迷失的挂起Excel实例。以下代码仅检查名称为“Excel”的进程,然后将进程的MainWindowTitle属性写入字符串。这种与进程的“交互”似乎使Windows赶上并中止Excel的冻结实例。

在我开发的加载项退出时触发其卸载事件之前,我运行以下方法。每次它都会删除任何挂起的Excel实例。老实说,我不完全确定为什么这有效,但它对我很有效,可以放置在任何Excel应用程序的末尾,而无需担心双点、Marshal.ReleaseComObject或杀死进程。我非常希望得到任何有关为什么这有效的建议。

public static void SweepExcelProcesses()
{           
            if (Process.GetProcessesByName("EXCEL").Length != 0)
            {
                Process[] processes = Process.GetProcesses();
                foreach (Process process in processes)
                {
                    if (process.ProcessName.ToString() == "excel")
                    {                           
                        string title = process.MainWindowTitle;
                    }
                }
            }
}

1

正如一些人可能已经写过的那样,你关闭Excel(对象)的方式不仅很重要,而且打开它的方式以及项目类型也很重要。

在WPF应用程序中,基本上相同的代码可以在没有或者非常少的问题的情况下工作。

我有一个项目,其中相同的Excel文件会根据不同的参数值被处理多次 - 例如,基于通用列表中的值进行解析。

我将所有与Excel相关的函数放在基类中,并将解析器放在子类中(不同的解析器使用公共的Excel函数)。我不希望每个通用列表项都重新打开和关闭Excel,因此我只在基类中打开了一次,然后在子类中关闭。当我将代码移植到桌面应用程序中时遇到了问题。我尝试了许多上述提到的解决方案。`GC.Collect()`已经实现了两次,就像建议的那样。

我决定将打开Excel的代码移动到一个子类中。现在不是只打开一次,而是为每个项目创建一个新对象(基类)并在结尾关闭它。虽然会有一些性能损失,但根据几个测试,Excel进程在没有问题的情况下关闭(在调试模式下),所以也清除了临时文件。如果有更新,我会继续测试并编写更多内容。

重要的是:您还必须检查初始化代码,特别是如果您有许多类等。


1

这似乎被过度复杂化了。根据我的经验,让Excel正常关闭只需要三个关键步骤:

1:确保没有剩余的对你创建的Excel应用程序的引用(你应该只有一个,把它设置成null)

2:调用GC.Collect()

3:Excel必须关闭,要么是用户手动关闭程序,要么是你在Excel对象上调用Quit。(请注意,即使Excel不可见,Quit将像用户尝试关闭该程序一样运行,并且如果存在未保存的更改,则会显示确认对话框。用户可能会按取消键,然后Excel将未关闭。)

1需要在2之前发生,但是3随时可以发生。

一种实现方法是使用自己的类包装Interop Excel对象,在构造函数中创建Interop实例,并使用Dispose实现IDisposable,Dispose看起来像这样:

这将从程序的角度清理掉Excel。一旦Excel关闭(用户手动关闭或通过调用Quit关闭),该进程将消失。如果程序已关闭,则进程将在GC.Collect()调用时消失。

(我不确定这有多重要,但在GC.Collect()调用后您可能需要一个GC.WaitForPendingFinalizers()调用,但这并非严格必要以摆脱Excel进程。)
多年来,这对我没有任何问题。但请记住,虽然它有效,但您实际上必须正常关闭才能使其起作用。如果您在清理Excel之前中断程序(通常是通过在调试程序时按“停止”),则仍会累积excel.exe进程。

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