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

C# 如何正确清理Excel互操作对象?,c#,excel,interop,com-interop,C#,Excel,Interop,Com Interop,我在C应用程序类中使用Excel互操作,并在finally子句中放置了以下代码: while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { } excelSheet = null; GC.Collect(); GC.WaitForPendingFinalizers(); 虽然这种方法可以工作,但即使在我关闭Excel后,Excel.exe进程仍在后台。只有在手动关闭我的应用程序后,它才会

我在C应用程序类中使用Excel互操作,并在finally子句中放置了以下代码:

while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();
虽然这种方法可以工作,但即使在我关闭Excel后,Excel.exe进程仍在后台。只有在手动关闭我的应用程序后,它才会发布


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

这对我正在处理的项目有效:

excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;

我们了解到,在使用Excel COM对象时,将它的每个引用都设置为null非常重要。这包括单元格、工作表和所有内容。

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

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

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

Worksheet sheet = excelApp.Worksheets.Open(...);
...
Marshal.ReleaseComObject(sheet);
我不知道C在内部为Worksheets COM对象创建了一个包装器,我的代码没有发布这个包装器,因为我不知道这是为什么Excel没有卸载的原因

我在上找到了问题的解决方案,它还有一个很好的C中COM对象使用规则:


切勿将两点用于COM对象

因此,有了这些知识,进行上述工作的正确方法是:

Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);
验尸报告:

我希望每个读者都仔细阅读Hans Passant的回答,因为它解释了我和许多其他开发人员遇到的陷阱。当我几年前写下这个答案时,我不知道调试器对垃圾收集器的影响,并得出了错误的结论。为了历史的缘故,我保持我的答案不变,但请阅读此链接,不要走两个点的路:并且,Excel名称空间中的任何内容都需要发布。时期

你不能这样做:

Worksheet ws = excel.WorkBooks[1].WorkSheets[1];
你必须做些什么

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

然后是对象的发布。

我认为其中一些只是框架处理Office应用程序的方式,但我可能错了。有时,一些应用程序会立即清理进程,而另一些天则似乎要等到应用程序关闭。总的来说,我不再关注细节,只是确保一天结束时不会有任何额外的流程

还有,也许我过于简化了,但我想你可以

objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
DoSomeStuff(objBook);
SaveTheBook(objBook);
objBook.Close(false, Type.Missing, Type.Missing);
objExcel.Quit();

正如我前面所说的,我不倾向于关注Excel进程何时出现或消失的细节,但这对我来说通常是可行的。我也不喜欢将Excel进程保留在最短的时间内,但我可能只是对此有点偏执。

正如其他人所指出的,您需要为使用的每个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,请在释放应用程序对象之前退出

正如你所看到的,一旦你尝试做任何事情,即使是中等复杂的事情,这很快就会变得非常笨拙。我已经成功地使用一个简单的包装类开发了.NET应用程序,该类包装了Excel对象模型的一些简单操作打开工作簿、写入范围、保存/关闭工作簿等。包装类实现IDisposable,仔细地在它使用的每个对象上实现Marshal.ReleaseComObject,并且不会向应用程序的其余部分公开任何Excel对象

但是这种方法不能很好地适应更复杂的需求


这是.NET COM互操作的一大缺陷。对于更复杂的场景,我将认真考虑在VB6或其他非托管语言中编写ActiveX DLL,您可以将其与Office等OPro COM对象的所有交互委托。然后,您可以从.NET应用程序中引用此ActiveX DLL,因为只需发布此引用,事情就会简单得多。

您实际上可以干净地发布Excel应用程序对象,但必须小心

建议为您访问的每个COM对象维护一个命名引用,然后通过Marshal.finalEleaseComObject显式释放它,这在理论上是正确的,但不幸的是,在实践中很难管理。如果有人在任何地方滑动并使用两个点,或者通过for-each循环或任何其他类似的命令来迭代单元格,那么您将拥有未引用的COM对象并面临挂起的风险。在这种情况下,无法在代码中找到原因;您必须用肉眼查看所有代码,并希望找到原因,这对于大型项目来说几乎是不可能的任务

好消息是你实际上没有 必须维护对您使用的每个COM对象的命名变量引用。相反,先调用GC.Collect,然后调用GC.WaitForPendingFinalizers,释放所有通常不包含引用的次要对象,然后显式释放包含命名变量引用的对象

您还应该按与重要性相反的顺序释放命名引用:首先是范围对象,然后是工作表、工作簿,最后是Excel应用程序对象

例如,假设您有一个名为xlRng的范围对象变量、一个名为xlSheet的工作表变量、一个名为xlBook的工作簿变量和一个名为xlApp的Excel应用程序变量,那么您的清理代码可能如下所示:

// Cleanup
GC.Collect();
GC.WaitForPendingFinalizers();

Marshal.FinalReleaseComObject(xlRng);
Marshal.FinalReleaseComObject(xlSheet);

xlBook.Close(Type.Missing, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(xlBook);

xlApp.Quit();
Marshal.FinalReleaseComObject(xlApp);
在从.NET清理COM对象的大多数代码示例中,GC.Collect和GC.WaitForPendingFinalizers调用的次数是中的两倍:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
但是,除非您正在使用VisualStudioToolsforOffice VSTO,否则不需要这样做,因为它使用的终结器会导致在终结队列中提升整个对象图。在下一次垃圾回收之前,这些对象不会被释放。但是,如果您不使用VSTO,您应该能够只调用一次GC.Collect和GC.WaitForPendingFinalizers

我知道显式调用GC.Collect是一个禁忌,当然,两次调用听起来非常痛苦,但老实说,没有办法解决。通过正常操作,您将生成没有引用的隐藏对象,因此,除了调用GC.Collect之外,您无法通过任何其他方式释放这些对象

这是一个复杂的话题,但这真的是它的全部。为清理过程建立此模板后,您可以正常编码,而无需包装器等:-

我在这里有一个关于这方面的教程:


它是为VB.NET编写的,但不要因此而延迟,其原理与使用C时完全相同。

您需要注意,Excel对您运行的文化也非常敏感

您可能会发现,在调用Excel函数之前,需要将区域性设置为EN-US。 这并不适用于所有函数,但适用于其中一些函数

    CultureInfo en_US = new System.Globalization.CultureInfo("en-US"); 
    System.Threading.Thread.CurrentThread.CurrentCulture = en_US;
    string filePathLocal = _applicationObject.ActiveWorkbook.Path;
    System.Threading.Thread.CurrentThread.CurrentCulture = orgCulture;
即使您正在使用VSTO,这也适用

有关详细信息:

I一个有用的通用模板,可以帮助实现COM对象的正确处理模式,当COM对象超出范围时需要调用Marshal.ReleaseComObject:

用法:

模板:

参考:

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

我花了一些时间试图解决这个问题,当时XtremeVBTalk是最活跃、反应最灵敏的。这里是我的原始帖子的链接。下面是这篇文章的摘要,以及复制到这篇文章中的代码

使用Application.Quit和process.Kill关闭互操作进程在大部分情况下都有效,但如果应用程序发生灾难性崩溃,则会失败。也就是说,如果应用程序崩溃,Excel进程仍将松散运行。 解决方案是让操作系统通过使用Win32调用来处理进程的清理。当主应用程序死亡时,相关进程(如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\u OBJECT\u LIMIT\u KILL\u ON\u JOB\u CLOSE枚举值,该值定义为: 导致与作业关联的所有进程在 作业的最后一个句柄已关闭

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

使用代码


普通的开发人员,你的解决方案都不适合我, 所以我决定实施一个新的技巧

首先,让我们具体说明我们的目标是什么?=>在任务管理器中完成作业后不查看excel对象

嗯。不要让它挑战并开始销毁它,但是不要破坏其他并行运行的Excel操作系统Excel。 因此,获取当前处理器的列表并获取EXCEL进程的PID,然后一旦您的工作完成,我们将在进程列表中创建一个具有唯一PID的新来宾,找到并销毁该进程

<请记住,在excel作业期间,任何新的excel进程都将被检测为新的并被销毁> <更好的解决方案是捕获新创建的excel对象的PID并销毁它>

Process[] prs = Process.GetProcesses();
List<int> excelPID = new List<int>();
foreach (Process p in prs)
   if (p.ProcessName == "EXCEL")
       excelPID.Add(p.Id);

.... // your job 

prs = Process.GetProcesses();
foreach (Process p in prs)
   if (p.ProcessName == "EXCEL" && !excelPID.Contains(p.Id))
       p.Kill();
这就解决了我的问题,希望你的问题也能解决。

前言:我的答案包含两种解决方案,所以阅读时要小心,不要遗漏任何内容

对于如何卸载Excel实例,有不同的方法和建议,例如:

显式释放每个com对象 使用Marshal.FinalReleaseComObject 不忘含蓄 创建com对象。释放 每个创建的com对象,您都可以使用 规则 这里提到的两点中:

调用GC.Collect和 GC.WaitForPendingFinalizers要生成 CLR释放未使用的com对象*实际上,它是有效的,有关详细信息,请参阅我的第二个解决方案

正在检查com服务器应用程序 可能会显示一个等待的消息框 虽然我不是,但用户仍要回答 确定它可以阻止Excel 接近尾声,但我听说过几次 时代

将WM_关闭消息发送到主服务器 Excel窗口

执行有效的函数 在单独的AppDomain中使用Excel。 有些人相信这个例子 将在AppDomain关闭时关闭 卸载

杀死所有在excel互操作代码启动后实例化的excel实例

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

例如,昨天我发现,在我的一个使用excel的函数中,excel在函数结束后继续运行。我什么都试过了!我彻底检查了整个函数10次,并为所有内容添加了Marshal.FinalReleaseComObject!我还有GC.Collect和GC.WaitForPendingFinalizers。我检查了隐藏的消息框。我试图将WM_关闭消息发送到Excel主窗口。我在一个单独的AppDomain中执行了我的函数,并卸载了该域。没什么用!关闭所有excel实例的选项是不合适的,因为如果用户在执行“我的函数”期间手动启动另一个excel实例(该函数也适用于excel),则该实例也将被“我的函数”关闭。我打赌用户不会高兴的!所以,老实说,这是一个蹩脚的选择,没有冒犯的家伙。所以我花了几个小时才找到了一个好的解决方案:通过主窗口的hWnd杀死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();
}
瞧!Excel已终止!:

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

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

2将使用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);
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将关闭=

此处接受的答案是正确的,但还要注意,不仅需要避免两个点引用,还需要避免通过索引检索的对象。您也不需要等到程序完成后才清理这些对象,最好创建函数,在可能的情况下,在完成这些对象后尽快清理它们。下面是我创建的一个函数,它为名为xlStyleHeader的样式对象指定一些属性:

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

这类事情在标准应用程序中并不是真正必要的,标准应用程序在自我清理方面做得很好,但在ASP.NET应用程序中,如果您错过了其中一个,无论您多么频繁地调用垃圾收集器,Excel仍将在您的服务器上运行


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

我不敢相信这个问题已经困扰世界5年了。。。。如果已创建应用程序,则需要先将其关闭,然后再删除链接

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程序不是您直接控制的一部分。因此,如果链接被释放,它将保持打开状态


很好的编程每个人~~

添加Excel不关闭的原因,即使在读取时创建每个对象的直接引用,创建也是“For”循环

For Each objWorkBook As WorkBook in objWorkBooks 'local ref, created from ExcelApp.WorkBooks to avoid the double-dot
   objWorkBook.Close 'or whatever
   FinalReleaseComObject(objWorkBook)
   objWorkBook = Nothing
Next 

'The above does not work, and this is the workaround:

For intCounter As Integer = 1 To mobjExcel_WorkBooks.Count
   Dim objTempWorkBook As Workbook = mobjExcel_WorkBooks.Item(intCounter)
   objTempWorkBook.Saved = True
   objTempWorkBook.Close(False, Type.Missing, Type.Missing)
   FinalReleaseComObject(objTempWorkBook)
   objTempWorkBook = Nothing
Next

当上述所有内容都不起作用时,请尝试给Excel一些时间来关闭其工作表:

app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();

...
FinalReleaseComObject(app);
这看起来真像是李 这太复杂了。根据我的经验,只有三件关键的事情可以让Excel正常关闭:

1:确保您创建的excel应用程序没有剩余的引用,您应该只有一个引用;将其设置为null

2:打电话给GC。对方付费

3:必须通过用户手动关闭程序或通过调用Excel对象上的Quit来关闭Excel。请注意,“退出”的功能与用户试图关闭程序一样,如果有未保存的更改,即使Excel不可见,也会显示一个确认对话框。用户可以按“取消”,Excel将不会被关闭

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

实现这一点的一种方法是使用您自己的类包装interop Excel对象,在构造函数中创建interop实例,并使用Dispose实现IDisposable

if (!mDisposed) {
   mExcel = null;
   GC.Collect();
   mDisposed = true;
}
这将从程序方面清理excel。一旦用户或通过调用Quit手动关闭Excel,该过程将消失。如果程序已经关闭,那么进程将在GC.Collect调用中消失

我不确定它有多重要,但您可能希望在GC.Collect调用之后调用GC.WaitForPendingFinalizers,但严格来说,不需要取消Excel进程


多年来,这对我来说毫无问题。请记住,虽然这是可行的,但实际上您必须优雅地关闭它才能工作。如果在清除excel之前中断程序,通常在调试程序时单击“停止”键,您仍将获得累积的excel.exe进程。

使用Word/excel互操作应用程序时应非常小心。在尝试了所有的解决方案之后,我们仍然有很多WinWord进程在服务器上打开,有2000多个用户

在解决这个问题几个小时后,我意识到如果我在不同的线程上同时使用Word.ApplicationClass.Document.open打开多个文档,IIS工作进程w3wp.exe将崩溃,所有WinWord进程都将打开


因此,我想这个问题没有绝对的解决办法,但改用其他方法,如开发。

不要在COM对象上使用两点,这是避免COM引用泄漏的一条重要经验法则,但Excel PIA可能导致泄漏的方式比乍一看要多

其中一种方法是订阅由任何Excel对象模型的COM对象公开的任何事件

例如,订阅应用程序类的WorkbookOpen事件

关于COM事件的一些理论 COM类通过回调接口公开一组事件。为了订阅事件,客户机代码可以简单地注册一个实现回调接口的对象,COM类将调用其方法来响应特定事件。由于回调接口是COM接口,因此实现对象有责任减少它作为任何事件处理程序的参数接收的任何COM对象的引用计数

Excel PIA如何公开COM事件 Excel PIA将Excel应用程序类的COM事件公开为常规.NET事件。每当客户机代码订阅以“a”为重点的.NET事件时,PIA就会创建一个实现回调接口的类的实例,并将其注册到Excel中

因此,许多回调对象在Excel中注册,以响应来自.NET代码的不同订阅请求。每个事件订阅一个回调对象

事件处理的回调接口意味着,PIA必须为每个.NET事件订阅请求订阅所有接口事件。它不能挑三拣四。在接收到事件回调时,回调对象检查关联的.NET事件处理程序是否对当前事件感兴趣,然后调用处理程序或以静默方式忽略回调

对COM实例引用计数的影响 所有这些回调对象都不会减少它们作为任何回调方法的参数接收的任何COM对象的引用计数,即使对于那些被静默忽略的方法也是如此。它们完全依赖垃圾收集器释放COM对象

由于GC运行是不确定的,这可能会导致Excel进程延迟超过预期的时间,并造成“内存泄漏”的印象

解决方案 到目前为止,唯一的解决方案是避免使用PIA的COM类事件提供程序,并编写自己的事件提供程序来确定释放COM对象


对于应用程序类,可以通过实现AppEvents接口,然后使用在Excel中注册实现来实现。应用程序类以及所有使用回调机制公开事件的COM对象都实现了IConnectionPointContainer接口。

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

我花了几个小时尝试了几种方法。所有这些都是很好的想法,但我最终发现了我的错误:如果你不释放所有对象,上面的任何方法都不能像我这样帮助你。确保释放所有对象,包括范围1

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

这些选项都在一起。

关于发布COM对象的一篇好文章是MSDN

我提倡的方法是,如果Excel.Interop引用是非局部变量,则将它们设为null,然后调用GC.Collect和GC.WaitForPendingFinalizers两次。本地范围的互操作变量将自动处理

这样就不需要为每个COM对象保留命名引用

以下是本文中的一个示例:

public class Test {

    // These instance variables must be nulled or Excel will not quit
    private Excel.Application xl;
    private Excel.Workbook book;

    public void DoSomething()
    {
        xl = new Excel.Application();
        xl.Visible = true;
        book = xl.Workbooks.Add(Type.Missing);

        // These variables are locally scoped, so we need not worry about them.
        // Notice I don't care about using two dots.
        Excel.Range rng = book.Worksheets[1].UsedRange;
    }

    public void CleanUp()
    {
        book = null;
        xl.Quit();
        xl = null;

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
这些话直接来自文章:

public class Test {

    // These instance variables must be nulled or Excel will not quit
    private Excel.Application xl;
    private Excel.Workbook book;

    public void DoSomething()
    {
        xl = new Excel.Application();
        xl.Visible = true;
        book = xl.Workbooks.Add(Type.Missing);

        // These variables are locally scoped, so we need not worry about them.
        // Notice I don't care about using two dots.
        Excel.Range rng = book.Worksheets[1].UsedRange;
    }

    public void CleanUp()
    {
        book = null;
        xl.Quit();
        xl = null;

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
在几乎所有的情况下,清空RCW引用并强制垃圾收集都会正确地清理。如果还调用GC.WaitForPendingFinalizers,垃圾收集将尽可能具有确定性。也就是说,在第二次调用WaitForPendingFinalizers返回时,您将非常确定对象何时被清理。也可以使用Marshal.ReleaseComObject。但是,请注意,您不太可能需要使用此方法


传统上,我遵循的建议是。然而,为了使这个答案与最新的选项保持同步,我认为我未来的所有项目都将使用NetOffice库

是Office PIA的完全替代品,完全不可知版本。它是一个托管COM包装器的集合,可以处理在.NET中使用Microsoft Office时经常引起此类问题的清理

一些主要特点是:

大多数独立于版本和依赖于版本的功能都有文档记录 无依赖项 无PIA 没有注册 无VSTO
我与该项目没有任何关联;我只是真诚地欣赏头痛的明显减少。

正如一些人可能已经写过的那样,如何关闭Excel对象并不重要;如何打开它以及项目的类型也很重要

在WPF应用程序中,基本上相同的代码在工作时没有或很少出现问题

我有一个项目,在该项目中,同一个Excel文件针对不同的参数值进行了多次处理-例如,根据通用列表中的值对其进行解析

我将所有与Excel相关的函数放入基类,并将解析器放入一个子类中。不同的解析器使用常见的Excel函数。我不希望在一般列表中的每个项目都再次打开和关闭Excel,所以我只在基类中打开它一次,在子类中关闭它一次。我在将代码移动到桌面应用程序时遇到问题。我已经尝试了上述许多解决方案。Collect以前已经实现过,是建议的两倍

然后我决定将打开Excel的代码移动到一个子类中。我不再只打开一次,而是创建一个新的对象基类,为每个项目打开Excel,并在最后关闭它。有一些性能损失,但根据一些测试,Excel进程在调试模式下会顺利关闭,因此临时文件也会被删除。我会继续测试,如果我能得到一些更新,我会写更多的东西

底线是:您还必须检查初始化代码,特别是如果您有许多类,等等

试过之后

按相反顺序释放COM对象 在末尾添加两次GC.Collect和GC.WaitForPendingFinalizers 不超过两个点 关闭工作簿并退出应用程序 在发布模式下运行 对我有效的最终解决方案是移动一组

GC.Collect();
GC.WaitForPendingFinalizers();
我们将其添加到包装器的函数末尾,如下所示:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}

两点法则对我不起作用。在我的例子中,我创建了一个清理资源的方法,如下所示:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}

我就是这么做的。。。但我还是遇到了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();
我的解决方案

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

被接受的答案对我不起作用。析构函数中的以下代码完成了此工作

if (xlApp != null)
{
    xlApp.Workbooks.Close();
    xlApp.Quit();
}

System.Diagnostics.Process[] processArray = System.Diagnostics.Process.GetProcessesByName("EXCEL");
foreach (System.Diagnostics.Process process in processArray)
{
    if (process.MainWindowTitle.Length == 0) { process.Kill(); }
}
首先-您不必调用Marshal.ReleaseComObject。。。或者元帅。最终的EleaseComObject。。。在执行Excel互操作时。这是一个令人困惑的反模式,但任何有关此的信息(包括来自Microsoft的信息)都表明您必须手动从.NET中释放COM引用是不正确的。事实上,.NET运行时和垃圾收集器正确地跟踪和清理COM引用。对于您的代码,这意味着您可以在。。。在顶部打圈

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

第三,在调试器下运行时,本地引用将被人为地保持活动状态,直到方法结束,以便本地变量检查工作。因此,GC.Collect调用对于从同一方法中清除像rng.Cells这样的对象是无效的。您应该将执行COM互操作的代码从GC清理拆分为单独的方法。这对我来说是一个关键的发现,来自@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和堆栈溢出的帖子,尤其是这个问题


最后说服我仔细观察并找出正确建议的是博客文章,同时发现调试器下的引用存在问题,这让我之前的测试感到困惑。

我目前正在从事办公自动化工作,并偶然发现了一个解决方案,它每次对我都有效。它很简单,不涉及终止任何进程

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

}
似乎只要在当前活动进程中循环,并以任何方式“访问”一个打开的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;
                    }
                }
            }
}


您是否试图在不关闭应用程序的情况下关闭Excel.exe?我不确定是否完全理解您的问题。我正在尝试确保正确处理非托管互操作对象。因此,即使用户已经完成了从应用程序创建的Excel电子表格,也不存在Excel进程。如果您可以通过生成XML Excel文件来尝试,否则请考虑VSTOUN/托管内存管理:这能很好地转化为Excel吗?参见下面的来自微软的支持文章的答案,他们具体给出了这个问题的解决方案:如何在名称空间中释放aobut xlRange.Interior.Color例如.Interior。。。另一方面,颜色不会导致其来自System.Drawing.Color,IIR实际上是Excel颜色而不是.Net颜色。你只是路过一段很长的路。还有工作手册。需要发布,工作表。。。不是这样,两个点坏,一个点好的规则是胡说八道。此外,您不必发布工作簿、工作表、范围,这是GC的工作。您永远不要忘记的唯一一件事是对唯一的Excel.Application对象调用Quit。调用Quit后,将Excel.Application对象置空,调用GC.Collect;GC.WaitForPendingFinalizers;两次。可以在ExtremeVBTalk.NET Office Automation论坛上找到相关讨论,此处:。如果所有其他方法都失败,则可以使用Process.Kill作为此处所述的最后手段:很高兴它成功了Richard.:-这里有一个微妙的例子,仅仅避免两点不足以防止问题的发生:以下是微软的米莎·施尼森(Misha Shneerson)在评论日期2010年10月7日内对这个话题的看法。我希望这能帮助我们解决一个长期存在的问题。在finally块中进行垃圾收集和释放调用安全吗?那么我建议不要使用COM中的Excel,这样就省去了所有的麻烦。Excel 2007格式不用打开Excel就可以使用,太棒了。我不明白两点是什么意思。你能解释一下吗?这意味着,你不应该使用模式comObject.Property.Property属性你看到两点了吗?。而是将comObject.Property分配给变量,并使用和处置该变量。上述规则的更正式版本可能是在使用变量之前将com对象分配给变量。这包括作为另一个com对象属性的com对象。@尼克:实际上,您不需要任何类型的清理,因为垃圾收集器将为您完成清理。您需要做的唯一一件事是将每个COM对象分配给它自己的变量,以便GC知道它。@VSS这是胡说八道,GC是干净的

因为包装器变量是由.net framework生成的,所以它会对所有内容进行备份。GC可能会花很长时间来清理它。在大量互操作之后调用GC.Collect不是一个坏主意。是的,这一个很好。虽然现在FinalEleaseComObject可用,但我认为这段代码可以更新。对于每个对象都有一个using块仍然很烦人。请看这里的另一个选择。遗憾的是,这条线索已经太老了,以至于你的优秀答案似乎远远低于此,我认为这是它没有被更多次投票的唯一原因……我必须承认,当我第一次阅读你的答案时,我认为这是一个巨大的障碍。经过大约6个小时的努力,一切都公布了,我没有双点,等等,我现在认为你的答案是天才。谢谢你。在我发现这个之前的几天里,我一直在与一个无论如何都不会关闭的Excel进行斗争。太棒了。@nightcoder:回答太棒了,非常详细。您指出的关于调试模式的评论是非常真实和重要的。在发布模式下使用相同的代码通常可以。然而,Process.Kill只有在使用自动化时才有效,而不是当您的程序在Excel中运行时,例如作为托管COM加载项时。@DanM:您的方法是100%正确的,永远不会失败。通过将所有变量保持在方法的本地,所有引用实际上都是.NET代码无法访问的。这意味着,当finally部分调用GC.Collect时,所有COM对象的终结器都将被确定地调用。然后,每个终结器对要终结的对象调用Marshal.finalEleaseComObject。因此,你的方法很简单,也很简单。不用担心使用这个。唯一需要注意的是:如果使用VSTO(我怀疑您是),您需要调用GC.Collect&GC.WaitForPendingFinalizers两次。另外一个原因是,在不首先释放之前的值的情况下重新使用引用。谢谢。我还使用了a,因为您的解决方案对我有效。+1谢谢。另外,请注意,如果您有一个Excel范围的对象,并且希望在该对象的生命周期内更改该范围,我发现我必须在重新分配它之前释放该对象,这使得代码有点混乱!我讨厌随意的等待。但是+1,因为您认为需要等待工作簿关闭是正确的。另一种方法是在循环中轮询工作簿集合,并使用任意等待作为循环超时。非常好!我想,因为我使用的是Office2007,所以有人代表我做了一些清理工作。我已经进一步使用了advice,但没有像您在这里建议的那样存储变量,EXCEL.EXE确实会退出,但我可能很幸运,如果我有任何进一步的问题,我一定会查看我代码的这部分=这个答案有效,并且比公认的答案方便得多。不用担心COM对象的两点,不用封送。在发布内部,检查Null Joe的答案。这将避免不必要的null异常。我已经测试了很多方法。这是有效发布Excel的唯一方法。显式杀死可能正在为其他COM客户端服务的进程外COM服务器是一个可怕的想法。如果你是诉诸这是因为你的COM协议被破坏。COM服务器不应被视为常规窗口应用程序否,尤其是如果您希望允许用户与COM应用程序Excel交互,在这种情况下,OP没有指定是否需要用户交互,但没有提及关闭它。你只需要让调用方放手,用户就可以在关闭(比如)单个文件时退出Excel。这对我来说非常有效-当我只有objExcel.quit时崩溃,但当我事先也做了objExcel.Application.quit时,完全关闭。这应该被标记为答案,真的。NetOffice将所有这些复杂性抽象化了。我已经使用NetOffice编写了相当长时间的excel加载项,它工作得非常好。唯一需要考虑的是,如果不显式地处理使用过的对象,那么退出应用程序时会执行它,因为它仍然跟踪它们。因此,NetOffice的经验法则是始终对每个Excel对象(如单元格、区域或工作表等)使用模式。我发现我不需要将ComObject置零,只需要退出-关闭和最终EleaseComObject。我真不敢相信这就是我在工作中所缺少的一切。伟大的这是一种更复杂的方法它与我正在使用的类似:但需要更改。使用这种方法的问题是,在运行excel的机器上打开excel时,它通常会在左侧栏中显示警告,表示类似excel的内容被错误关闭:不太优雅。我认为这篇文章中的一个早期建议是可取的。被否决是因为这是一个丑陋的黑客而不是一个解决方案。除了这一个,这页上几乎所有的答案都是错误的。他们工作,但太多的工作。忽略2点规则。让GC来完成您的工作
或者你。来自.Net大师汉斯·帕桑(Hans Passant)的补充证据:戈弗特,找到正确的方法真是松了一口气。非常感谢。
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
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;
                    }
                }
            }
}