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

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

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

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


当前回答

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

其他回答

我找到了一个有用的通用模板,它可以帮助实现COM对象的正确处置模式,这些对象在超出范围时需要调用Marshal.ReleaseComObject:

用法:

using (AutoReleaseComObject<Application> excelApplicationWrapper = new AutoReleaseComObject<Application>(new Application()))
{
    try
    {
        using (AutoReleaseComObject<Workbook> workbookWrapper = new AutoReleaseComObject<Workbook>(excelApplicationWrapper.ComObject.Workbooks.Open(namedRangeBase.FullName, false, false, missing, missing, missing, true, missing, missing, true, missing, missing, missing, missing, missing)))
        {
           // do something with your workbook....
        }
    }
    finally
    {
         excelApplicationWrapper.ComObject.Quit();
    } 
}

模板:

public class AutoReleaseComObject<T> : IDisposable
{
    private T m_comObject;
    private bool m_armed = true;
    private bool m_disposed = false;

    public AutoReleaseComObject(T comObject)
    {
        Debug.Assert(comObject != null);
        m_comObject = comObject;
    }

#if DEBUG
    ~AutoReleaseComObject()
    {
        // We should have been disposed using Dispose().
        Debug.WriteLine("Finalize being called, should have been disposed");

        if (this.ComObject != null)
        {
            Debug.WriteLine(string.Format("ComObject was not null:{0}, name:{1}.", this.ComObject, this.ComObjectName));
        }

        //Debug.Assert(false);
    }
#endif

    public T ComObject
    {
        get
        {
            Debug.Assert(!m_disposed);
            return m_comObject;
        }
    }

    private string ComObjectName
    {
        get
        {
            if(this.ComObject is Microsoft.Office.Interop.Excel.Workbook)
            {
                return ((Microsoft.Office.Interop.Excel.Workbook)this.ComObject).Name;
            }

            return null;
        }
    }

    public void Disarm()
    {
        Debug.Assert(!m_disposed);
        m_armed = false;
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
#if DEBUG
        GC.SuppressFinalize(this);
#endif
    }

    #endregion

    protected virtual void Dispose(bool disposing)
    {
        if (!m_disposed)
        {
            if (m_armed)
            {
                int refcnt = 0;
                do
                {
                    refcnt = System.Runtime.InteropServices.Marshal.ReleaseComObject(m_comObject);
                } while (refcnt > 0);

                m_comObject = default(T);
            }

            m_disposed = true;
        }
    }
}

参考:

http://www.deez.info/sengelha/2005/02/11/useful-idisposable-class-3-autoreleasecomobject/

Excel不会退出,因为应用程序仍保留对COM对象的引用。

我猜您调用了COM对象的至少一个成员,而没有将其分配给变量。

对我来说,它是excelApp.Worksheets对象,我直接使用它,而没有将它分配给变量:

Worksheet sheet = excelApp.Worksheets.Open(...);
...
Marshal.ReleaseComObject(sheet);

我不知道C#在内部为WorksheetsCOM对象创建了一个包装器,但我的代码没有发布它(因为我不知道),这也是Excel没有卸载的原因。

我在这个页面上找到了我的问题的解决方案,它也有一个关于C#中COM对象用法的好规则:

切勿对COM对象使用两点。


因此,有了这些知识,正确的方法是:

Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);

尸检后更新:

我希望每一位读者都能非常仔细地阅读Hans Passant的回答,因为它解释了我和许多其他开发人员偶然遇到的陷阱。几年前我写这个答案时,我不知道调试器对垃圾收集器的影响,得出了错误的结论。为了历史起见,我的答案保持不变,但请阅读此链接,不要走“两点”的道路:了解.NET中的垃圾收集和使用IDisposable清理Excel互操作对象

只是为了给这里列出的众多解决方案添加另一个解决方案,使用C++/ATL自动化(我想您可以使用类似于VB/C#??的东西)

Excel::_ApplicationPtr pXL = ...
  :
SendMessage ( ( HWND ) m_pXL->GetHwnd ( ), WM_DESTROY, 0, 0 ) ;

这对我来说很有魅力。。。

Excel命名空间中的任何内容都需要发布。时期

你不能这样做:

Worksheet ws = excel.WorkBooks[1].WorkSheets[1];

你必须这样做

Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];

随后释放物体。

我的解决方案

[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();
}