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

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

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

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


当前回答

首先,在执行Excel互操作时,您永远不必调用Marshal.ReleaseComObject(…)或Marshal.FinalReleaseComObject(.)。这是一个令人困惑的反模式,但任何有关此的信息(包括来自Microsoft的信息)都是不正确的,这些信息表明您必须从.NET手动释放COM引用。事实上,.NET运行时和垃圾收集器正确地跟踪和清理COM引用。对于您的代码,这意味着您可以删除顶部的整个`while(…)循环。

其次,如果要确保在进程结束时清理进程外COM对象的COM引用(以便Excel进程关闭),则需要确保垃圾收集器运行。您可以通过调用GC.Collect()和GC.WaitForPendingFinalizers()来正确地执行此操作。两次调用此操作是安全的,并且可以确保周期也被彻底清理(尽管我不确定是否需要,我希望能有一个示例来说明这一点)。

第三,当在调试器下运行时,本地引用将被人为地保持活动状态,直到方法结束(以便本地变量检查工作)。因此,GC.Collect()调用对于从同一方法中清除rng.Cells等对象无效。您应该将执行GC清理中的COM互操作的代码拆分为单独的方法。(这是我的一个关键发现,来自@nightcoder在这里发布的答案的一部分。)

因此,一般模式为:

Sub WrapperThatCleansUp()

    ' NOTE: Don't call Excel objects in here... 
    '       Debugger would keep alive until end, preventing GC cleanup

    ' Call a separate function that talks to Excel
    DoTheWork()

    ' Now let the GC clean up (twice, to clean up cycles too)
    GC.Collect()    
    GC.WaitForPendingFinalizers()
    GC.Collect()    
    GC.WaitForPendingFinalizers()

End Sub

Sub DoTheWork()
    Dim app As New Microsoft.Office.Interop.Excel.Application
    Dim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()
    Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")
    app.Visible = True
    For i As Integer = 1 To 10
        worksheet.Cells.Range("A" & i).Value = "Hello"
    Next
    book.Save()
    book.Close()
    app.Quit()

    ' NOTE: No calls the Marshal.ReleaseComObject() are ever needed
End Sub

关于这个问题有很多虚假信息和困惑,包括MSDN和Stack Overflow上的许多帖子(尤其是这个问题!)。

最终说服我仔细研究并找出正确建议的是博客文章Marshal.ReleaseComObject Considered Dangerous,以及发现在调试器下引用保持活动的问题,这让我之前的测试感到困惑。

其他回答

这里公认的答案是正确的,但也要注意,不仅需要避免“双点”引用,还需要避免通过索引检索的对象。您也不需要等到程序完成后才能清理这些对象,最好创建函数,以便在可能的情况下,在完成这些对象后立即清理它们。下面是我创建的一个函数,用于分配名为xlStyleHeader的Style对象的一些财产:

public Excel.Style xlStyleHeader = null;

private void CreateHeaderStyle()
{
    Excel.Styles xlStyles = null;
    Excel.Font xlFont = null;
    Excel.Interior xlInterior = null;
    Excel.Borders xlBorders = null;
    Excel.Border xlBorderBottom = null;

    try
    {
        xlStyles = xlWorkbook.Styles;
        xlStyleHeader = xlStyles.Add("Header", Type.Missing);

        // Text Format
        xlStyleHeader.NumberFormat = "@";

        // Bold
        xlFont = xlStyleHeader.Font;
        xlFont.Bold = true;

        // Light Gray Cell Color
        xlInterior = xlStyleHeader.Interior;
        xlInterior.Color = 12632256;

        // Medium Bottom border
        xlBorders = xlStyleHeader.Borders;
        xlBorderBottom = xlBorders[Excel.XlBordersIndex.xlEdgeBottom];
        xlBorderBottom.Weight = Excel.XlBorderWeight.xlMedium;
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        Release(xlBorderBottom);
        Release(xlBorders);
        Release(xlInterior);
        Release(xlFont);
        Release(xlStyles);
    }
}

private void Release(object obj)
{
    // Errors are ignored per Microsoft's suggestion for this type of function:
    // http://support.microsoft.com/default.aspx/kb/317109
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
    }
    catch { } 
}

请注意,我必须将xlBorders[Excel.XlBordersIndex.xlEdgeBottom]设置为一个变量,以清除该变量(不是因为两个点,这两个点指的是不需要释放的枚举,而是因为我所指的对象实际上是一个需要释放的Border对象)。

在标准应用程序中,这种事情并不是真正必要的,因为这些应用程序在清理后会做得很好,但在ASP.NET应用程序中如果您错过了其中的一个,无论您调用垃圾收集器的频率如何,Excel仍将在您的服务器上运行。

编写这段代码时,在监视任务管理器的同时,它需要对细节和许多测试执行进行大量关注,但这样做可以省去在代码页中拼命搜索以查找遗漏的一个实例的麻烦。当在循环中工作时,这一点尤其重要,因为您需要释放对象的每个实例,即使它每次循环都使用相同的变量名。

我的解决方案

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

更新:添加了C#代码,并链接到Windows作业

我花了一些时间试图解决这个问题,当时XtremeVBTalk是最活跃和响应最快的。这里有一个链接到我的原始文章,“即使应用程序崩溃,也要干净地关闭Excel互操作过程”。下面是文章的摘要,以及复制到本文的代码。

使用Application.Quit()和process.Kill()关闭Interop进程在很大程度上是有效的,但如果应用程序发生灾难性崩溃,则会失败。也就是说,如果应用程序崩溃,Excel进程仍将运行松散。解决方案是让操作系统使用Win32调用通过Windows作业对象处理进程的清理。当主应用程序失效时,相关的进程(即Excel)也将终止。

我发现这是一个干净的解决方案,因为操作系统正在进行真正的清理工作。您所要做的就是注册Excel进程。

Windows作业代码

包装Win32 API调用以注册互操作进程。

public enum JobObjectInfoType
{
    AssociateCompletionPortInformation = 7,
    BasicLimitInformation = 2,
    BasicUIRestrictions = 4,
    EndOfJobTimeInformation = 6,
    ExtendedLimitInformation = 9,
    SecurityLimitInformation = 5,
    GroupInformation = 11
}

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
    public int nLength;
    public IntPtr lpSecurityDescriptor;
    public int bInheritHandle;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
    public Int64 PerProcessUserTimeLimit;
    public Int64 PerJobUserTimeLimit;
    public Int16 LimitFlags;
    public UInt32 MinimumWorkingSetSize;
    public UInt32 MaximumWorkingSetSize;
    public Int16 ActiveProcessLimit;
    public Int64 Affinity;
    public Int16 PriorityClass;
    public Int16 SchedulingClass;
}

[StructLayout(LayoutKind.Sequential)]
struct IO_COUNTERS
{
    public UInt64 ReadOperationCount;
    public UInt64 WriteOperationCount;
    public UInt64 OtherOperationCount;
    public UInt64 ReadTransferCount;
    public UInt64 WriteTransferCount;
    public UInt64 OtherTransferCount;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
    public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
    public IO_COUNTERS IoInfo;
    public UInt32 ProcessMemoryLimit;
    public UInt32 JobMemoryLimit;
    public UInt32 PeakProcessMemoryUsed;
    public UInt32 PeakJobMemoryUsed;
}

public class Job : IDisposable
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern IntPtr CreateJobObject(object a, string lpName);

    [DllImport("kernel32.dll")]
    static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);

    private IntPtr m_handle;
    private bool m_disposed = false;

    public Job()
    {
        m_handle = CreateJobObject(null, null);

        JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
        info.LimitFlags = 0x2000;

        JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
        extendedInfo.BasicLimitInformation = info;

        int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
        IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
        Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);

        if (!SetInformationJobObject(m_handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length))
            throw new Exception(string.Format("Unable to set information.  Error: {0}", Marshal.GetLastWin32Error()));
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion

    private void Dispose(bool disposing)
    {
        if (m_disposed)
            return;

        if (disposing) {}

        Close();
        m_disposed = true;
    }

    public void Close()
    {
        Win32.CloseHandle(m_handle);
        m_handle = IntPtr.Zero;
    }

    public bool AddProcess(IntPtr handle)
    {
        return AssignProcessToJobObject(m_handle, handle);
    }

}

关于构造函数代码的注释

在构造函数中,info.LimitFlags=0x2000;被调用。0x2000是JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE枚举值,MSDN将此值定义为:

当作业的最后一个句柄关闭。

获取进程ID(PID)的额外Win32 API调用

    [DllImport("user32.dll", SetLastError = true)]
    public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

使用代码

    Excel.Application app = new Excel.ApplicationClass();
    Job job = new Job();
    uint pid = 0;
    Win32.GetWindowThreadProcessId(new IntPtr(app.Hwnd), out pid);
    job.AddProcess(Process.GetProcessById((int)pid).Handle);

正如其他人所指出的,您需要为使用的每个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,因为您只需要发布这一个引用,所以事情会变得更加简单。

在我的VSTO插件中新建应用程序对象后,我在关闭PowerPoint时遇到了同样的问题。我在这里尝试了所有的答案,但收效甚微。

这是我为我的案例找到的解决方案-不要使用“新应用程序”,ThisAddIn的AddInBase基类已经有了“应用程序”的句柄。如果你在需要的地方使用这个手柄(如果必须的话,让它保持静态),那么你不必担心清理它,PowerPoint也不会挂在近处。