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

while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();

尽管这种方法有效,但即使在我关闭Excel之后,Excel.exe进程仍处于后台。它只在我的应用程序被手动关闭后发布。

我做错了什么,或者是否有其他方法可以确保正确处理互操作对象?


当前回答

两点规则不适用于我。在我的案例中,我创建了一种方法来清理我的资源,如下所示:

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}

其他回答

这似乎太复杂了。根据我的经验,要让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实例,有不同的方法和建议,例如:

显式释放EVERY com对象使用Marshal.FinalReleaseComObject()(不忘含蓄创建的com对象)。释放每个创建的com对象,您可以使用这里提到的2点规则:如何正确清理Excel互操作对象?调用GC.Collect()和GC.WaitForPendingFinalizers()生成CLR释放未使用的com对象*(实际上,它是有效的,有关详细信息,请参阅我的第二个解决方案)检查com服务器应用程序可能会显示一个等待的消息框用户回答(尽管我不是确保它可以阻止Excel结束了,但我听说过几次次)向主服务器发送WM_CLOSE消息Excel窗口执行有效的函数在单独的AppDomain中使用Excel。有些人相信Excel实例当AppDomain为卸载。终止在excel互操作代码启动后实例化的所有excel实例。

但是!有时所有这些选项都没有帮助或不合适!

例如,昨天我发现在我的一个函数(与excel一起工作)中,excel在函数结束后仍在运行。我什么都试过了!我彻底检查了整个函数10次,并为所有内容添加了Marshal.FinalReleaseComObject()!我还有GC.Collect()和GC.WaitForPendingFinalizers()。我检查了隐藏的消息框。我试图将WM_CLOSE消息发送到Excel主窗口。我在一个单独的AppDomain中执行了我的函数,并卸载了该域。没什么帮助!关闭所有excel实例的选项是不合适的,因为如果用户在执行我的函数(该函数也适用于excel)期间手动启动另一个excel实例,则该实例也将被我的函数关闭。我打赌用户不会高兴的!所以,老实说,这是一个蹩脚的选择(没有冒犯的家伙)。所以我花了几个小时才找到了一个好的(我个人认为)解决方案:通过关闭excel进程的主窗口(这是第一个解决方案)。

下面是简单的代码:

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

/// <summary> Tries to find and kill process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <returns>True if process was found and killed. False if process was not found by hWnd or if it could not be killed.</returns>
public static bool TryKillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if(processID == 0) return false;
    try
    {
        Process.GetProcessById((int)processID).Kill();
    }
    catch (ArgumentException)
    {
        return false;
    }
    catch (Win32Exception)
    {
        return false;
    }
    catch (NotSupportedException)
    {
        return false;
    }
    catch (InvalidOperationException)
    {
        return false;
    }
    return true;
}

/// <summary> Finds and kills process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <exception cref="ArgumentException">
/// Thrown when process is not found by the hWnd parameter (the process is not running). 
/// The identifier of the process might be expired.
/// </exception>
/// <exception cref="Win32Exception">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="NotSupportedException">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="InvalidOperationException">See Process.Kill() exceptions documentation.</exception>
public static void KillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if (processID == 0)
        throw new ArgumentException("Process has not been found by the given main window handle.", "hWnd");
    Process.GetProcessById((int)processID).Kill();
}

正如您所看到的,根据Try Parse模式,我提供了两种方法(我认为这在这里是合适的):一种方法在无法终止进程时不会抛出异常(例如,进程不再存在),另一种方法则在进程未终止时抛出异常。此代码中唯一的弱点是安全权限。理论上,用户可能没有权限终止进程,但在所有情况下,99.99%的用户都有这样的权限。我还用一个客户帐户测试了它-它工作得很好。

因此,使用Excel的代码可以如下所示:

int hWnd = xl.Application.Hwnd;
// ...
// here we try to close Excel as usual, with xl.Quit(),
// Marshal.FinalReleaseComObject(xl) and so on
// ...
TryKillProcessByMainWindowHwnd(hWnd);

瞧!Excel已终止!:)

好的,让我们回到第二个解决方案,正如我在文章开头所承诺的那样。第二种解决方案是调用GC.Collect()和GC.WaitForPendingFinalizers()。是的,它们确实有效,但这里需要小心!很多人说(我也说过)调用GC.Collect()没有帮助。但是,如果仍然有对COM对象的引用,这将毫无帮助!GC.Collect()不起作用的最常见原因之一是以调试模式运行项目。在调试模式下,在方法结束之前,不再真正引用的对象不会被垃圾收集。因此,如果您尝试了GC.Collect()和GC.WaitForPendingFinalizers(),但没有帮助,请尝试执行以下操作:

1) 尝试在发布模式下运行项目,并检查Excel是否正确关闭

2) 将使用Excel的方法包装在单独的方法中。所以,不要像这样:

void GenerateWorkbook(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

你写道:

void GenerateWorkbook(...)
{
  try
  {
    GenerateWorkbookInternal(...);
  }
  finally
  {
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

private void GenerateWorkbookInternal(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
  }
}

现在,Excel将关闭=)

我完全遵循了这个。。。但我还是遇到了1000次问题中的1次。谁知道为什么。是时候拿出锤子了。。。

在Excel应用程序类实例化之后,我就掌握了刚刚创建的Excel进程。

excel = new Microsoft.Office.Interop.Excel.Application();
var process = Process.GetProcessesByName("EXCEL").OrderByDescending(p => p.StartTime).First();

然后,在完成上述所有COM清理之后,我确保该进程没有运行。如果它还在运行,就杀了它!

if (!process.HasExited)
   process.Kill();

这是唯一对我有效的方法

        foreach (Process proc in System.Diagnostics.Process.GetProcessesByName("EXCEL"))
        {
            proc.Kill();
        }

“千万不要在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接口。