提问



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


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


虽然这种工作,即使在我关闭Excel后,Excel.exe过程仍然在后台。只有在我的应用程序手动关闭后才会发布。


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

最佳参考


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


我猜你在调用COM对象的至少一个成员而不将其分配给变量。


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


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


我不知道内部C#为工作表 COM对象创建了一个包装器,它没有被我的代码释放(因为我没有意识到它),这也是Excel没有的原因卸载。


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



  切勿对COM对象使用两个点。






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


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

其它参考1


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


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


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


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


例如,假设您有一个名为xlRng的Range对象变量,名为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()调用是TWICE,如下所示:


GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();


但是,这不应该是必需的,除非您使用的是Visual Studio Tools for Office(VSTO),它使用终结器导致在终结队列中提升整个对象图。在 next 垃圾收集之前,不会释放此类对象。但是,如果您不使用VSTO,您应该只能调用GC.Collect()GC.WaitForPendingFinalizers()一次。


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


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


我在这里有一个教程:


使用VB.Net/COM Interop自动化Office程序[85]


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

其它参考2


前言:我的回答包含两个解决方案,因此在阅读时要小心,不要错过任何内容。


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



  • 显式释放每个com对象
    与Marshal.FinalReleaseComObject()
    (不要忘记隐含的
    创建了com对象)。要发布
    每个创建的com对象,您都可以使用
    这里提到的2点规则:

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

  • 调用GC.Collect()和
    GC.WaitForPendingFinalizers()来制作
    CLR发布未使用的com-objects *(实际上,它有效,请参阅我的第二个解决方案以获取详细信息)

  • 检查是否有com-server-application
    也许会显示一个消息框等待
    用户回答(虽然我不是
    确定它可以阻止Excel
    关闭,但我听说了几个
    次)

  • 向主要发送WM_CLOSE消息
    Excel窗口

  • 执行有效的功能
    使用Excel在单独的AppDomain中。
    有些人相信Excel实例
    将在AppDomain时关闭
    卸载。

  • 杀死我们的excel-interoping代码启动后实例化的所有excel实例。



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


例如,昨天我发现在我的一个函数中(使用excel)Excel在函数结束后继续运行。我尝试了一切!我彻底检查了整个函数10次,并为所有内容添加了Marshal.FinalReleaseComObject()!我还有GC.Collect()和GC.WaitForPendingFinalizers()。我检查了隐藏的消息框。我试图将WM_CLOSE消息发送到主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();
}


正如你所看到的,我提供了两种方法,根据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将关闭=)

其它参考3


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


我花了一些时间试图弄清楚这个问题,当时XtremeVBTalk是最活跃和最敏感的。这是我的原始帖子的链接,即使您的应用程序崩溃,干净地关闭Excel Interop进程。以下是帖子的摘要,以及复制到这篇文章的代码。 [87]



  • 使用Application.Quit()Process.Kill()关闭Interop进程大部分工作,但如果应用程序崩溃灾难则失败。即如果应用程序崩溃,Excel进程仍将运行松散。

  • 解决方案是让操作系统使用Win32调用通过Windows作业对象处理进程的清理。当您的主应用程序终止时,相关的进程(即Excel)也将终止。



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


Windows职务代码


包装Win32 API调用以注册Interop进程。


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;0x2000JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE枚举值,该值由MSDN定义为:




  导致与作业关联的所有进程在终止时终止
  关闭工作的最后一个句柄。[89]



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


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

其它参考4


这适用于我正在进行的项目:


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


我们了解到,当您完成Excel COM对象的每个引用时,将其设置为null非常重要。这包括细胞,表格和一切。

其它参考5


需要释放Excel命名空间中的任何内容。期间


你不能这样做:


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


你必须这样做


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


然后释放物体。

其它参考6


我找到了一个有用的通用模板,可以帮助实现COM对象的正确处理模式,当它们超出范围时需要Marshal.ReleaseComObject:[90]


用法:


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/[91]

其它参考7


我不敢相信这个问题困扰了全世界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程序不是您的直接控制的一部分。因此,如果链接被释放,它将保持打开状态!


好好编程大家~~

其它参考8


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


其次,如果要确保在进程结束时清除对进程外COM对象的COM引用(以便Excel进程关闭),则需要确保垃圾收集器运行。通过调用GC.Collect()GC.WaitForPendingFinalizers()可以正确执行此操作。调用这两次是安全的,并确保周期也被清理干净(虽然我不确定它是否需要,并且会欣赏一个显示这个的例子)。


第三,当在调试器下运行时,本地引用将被人为地保持Activity直到方法结束(以便局部变量检查工作)。因此GC.Collect()调用对于像rng.Cells这样的清除对象无效。您应该将执行COM interop的代码从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和Stack Overflow上的很多帖子(尤其是这个问题!)。


最终让我深入了解并找出正确建议的是博客文章 Marshal.ReleaseComObject Considered Dangerous 以及在调试器下发现引用保持Activity的问题,这让我早先的测试感到困惑。[[[92]

其它参考9


常见的开发人员,你的解决方案都没有为我工作,
所以我决定实施一个新的技巧


首先说明我们的目标是什么? >>在任务管理器中找到工作后不要查看excel对象


好。不要挑战并开始销毁它,但考虑不要破坏并行运行的其他实例os Excel。


因此,获取当前处理器的列表并获取EXCEL进程的PID,然后一旦您的工作完成,我们在进程列表中有一个具有唯一PID的新guest虚拟机,找到并销毁那个。


<请记住,在您的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();


这解决了我的问题,也希望你的问题。

其它参考10


这里接受的答案是正确的,但也请注意,不仅需要避免两点引用,还需要避免通过索引检索的对象。您也不需要等到程序完成后才能清理这些对象,最好在可能的情况下创建一个在它们完成后立即清理它们的函数。这是我创建的一个函数,它赋予一个名为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都会仍在您的服务器上运行。


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

其它参考11


为了添加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

其它参考12


这看起来确实过于复杂了。根据我的经验,让Excel正常关闭只有三个关键因素:


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


2:致电GC.Collect()


3:必须通过用户手动关闭程序或在Excel对象上调用Quit来关闭Excel。 (请注意,Quit的功能就像用户试图关闭程序一样,如果有未保存的更改,则会显示确认对话框,即使Excel不可见。用户可以按取消,然后Excel将还没有关闭。)


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


实现此目的的一种方法是使用您自己的类包装互操作Excel对象,在构造函数中创建互操作实例,并使用Dispose实现IDisposable类似于


if (!mDisposed) {
   mExcel = null;
   GC.Collect();
   mDisposed = true;
}


这将从你的程序方面清除excel。一旦Excel关闭(由用户手动或你调用Quit),该过程将消失。如果程序已经关闭,那么过程将会在GC.Collect()电话中消失。


(我不确定它有多重要,但你可能想在GC.Collect()调用之后调用GC.WaitForPendingFinalizers(),但并不是绝对必须摆脱Excel过程。)


多年来,这对我没有任何问题。请记住,虽然这有效,但实际上你必须优雅地关闭才能工作。如果在清理Excel之前中断程序(通常在调试程序时按停止),您仍将累积excel.exe进程。

其它参考13


尝试后



  1. 以相反的顺序释放COM对象

  2. 最后两次添加GC.Collect()GC.WaitForPendingFinalizers()

  3. 不超过两个点

  4. 关闭工作簿并退出应用程序

  5. 以发布模式运行



对我有用的最终解决方案是移动一组


GC.Collect();
GC.WaitForPendingFinalizers();


我们将函数的末尾添加到包装器中,如下所示:


private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

其它参考14


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


NetOffice是Office PIA的完全替代品,完全与版本无关。它是Managed COM包装器的集合,可以处理在.NET中使用Microsoft Office时经常导致此类令人头疼的清理。[94]


一些关键功能是:



  • 大部分与版本无关(并且记录了与版本相关的功能)

  • 无依赖

  • 没有PIA

  • 无需注册

  • 没有VSTO



我与该项目没有任何关系;我真的很欣赏头痛的严重减少。

其它参考15


¨°º¤ø¸拍摄Excel程序并咀嚼泡泡糖¸ø¤º°¨


public class MyExcelInteropClass
{
    Excel.Application xlApp;
    Excel.Workbook xlBook;

    public void dothingswithExcel() 
    {
        try { /* Do stuff manipulating cells sheets and workbooks ... */ }
        catch {}
        finally {KillExcelProcess(xlApp);}
    }

    static void KillExcelProcess(Excel.Application xlApp)
    {
        if (xlApp != null)
        {
            int excelProcessId = 0;
            GetWindowThreadProcessId(xlApp.Hwnd, out excelProcessId);
            Process p = Process.GetProcessById(excelProcessId);
            p.Kill();
            xlApp = null;
        }
    }

    [DllImport("user32.dll")]
    static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
}

其它参考16


您需要注意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,这也适用。


有关详细信息:http://support.microsoft.com/default.aspx?scid=kb; en-us; Q320369 [95]

其它参考17


我完全遵循了这一点......但我仍然遇到1000次中的问题。谁知道为什么。是时候拿出锤子......


在实例化Excel Application类之后,我得到了刚刚创建的Excel进程。


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


然后,一旦我完成了所有上述COM清理工作,我确保该过程不会运行。如果它还在运行,就杀掉它!


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

其它参考18


永远不要在COM对象中使用两个点是一个很好的经验法则,以避免COM引用的泄漏,但Excel PIA可能会导致泄漏,而不是一见钟情。


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


例如,订阅Application类的WorkbookOpen事件。


关于COM事件的一些理论



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


Excel PIA如何公开COM事件



Excel PIA将Excel Application类的COM事件公开为常规.NET事件。每当客户端代码订阅一个.NET事件(强调a)时,PIA就会创建实现回调接口的类的 实例并将其注册到Excel 。


因此,响应于不同的订阅请求f,许多回叫对象被注册到Excel.NET代码。每个事件订阅一个回调对象。


用于事件处理的回调接口意味着,PIA必须为每个.NET事件订阅请求订阅所有接口事件。它无法挑选。收到事件回调后,回调对象检查关联的.NET事件处理程序是否对当前事件感兴趣,然后调用处理程序或静默忽略回调。


对COM实例引用计数的影响



所有这些回调对象都不会减少它们为任何回调方法接收的任何COM对象(作为参数)的引用计数(即使对于被静默忽略的方法)也是如此。他们完全依赖CLR垃圾收集器来释放COM对象。[96]


由于GC运行是不确定的,这可能导致Excel进程的持续时间超过预期,并产生内存泄漏的印象。


解决方案



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


对于Application类,可以通过实现AppEvents接口,然后使用IConnectionPointContainer接口向Excel注册实现来完成。 Application类(以及使用回调机制公开事件的所有COM对象)实现IConnectionPointContainer接口。[97]

其它参考19


正如其他人指出的那样,您需要为您使用的每个Excel对象创建一个显式引用,并在该引用上调用Marshal.ReleaseComObject,如本KB文章中所述。您还需要使用try/finally确保始终调用ReleaseComObject,即使抛出异常也是如此。即而不是:[98]


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对象公开给应用程序的其余部分。


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


这是.NET COM Interop的一大缺陷。对于更复杂的场景,我会认真考虑用VB6或其他非托管语言编写ActiveX DLL,您可以将所有交互委托给Office等外部COM对象。然后,您可以从.NET应用程序中引用此ActiveX DLL,事情将变得更加容易,因为您只需要发布此一个引用。

其它参考20


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


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

...
FinalReleaseComObject(app);

其它参考21


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


我尝试了几种方法花了几个小时。所有这些都是很好的想法,但我终于找到了我的错误:如果你不发布所有对象,上面没有一种方法可以帮助你就像我的情况一样。确保你释放所有对象,包括范围一!


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


选项在这里。[99]

其它参考22


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


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

其它参考23


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


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


所以我想这个问题没有绝对的解决方案,但转而使用Office Open XML开发等其他方法。[100]

其它参考24


有关释放COM对象的好文章是 2.5释放COM对象(MSDN)。[101]


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


这消除了为每个 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();
    }
}


这些话直接来自文章:



  在几乎所有情况下,将RCW引用置为null并强制执行垃圾收集将正确清理。如果您还调用GC.WaitForPendingFinalizers,垃圾收集将尽可能确定。也就是说,在第二次调用WaitForPendingFinalizers之后,您将非常确定对象是否已被清理。作为替代方案,您可以使用Marshal.ReleaseComObject。但请注意,您不太可能需要使用这种方法。


其它参考25


我的解决方案


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

其它参考26


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


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

其它参考27


我目前正致力于办公自动化,并偶然发现了一个适用于我的解决方案。它很简单,不涉及杀死任何进程。


似乎只是通过循环遍历当前Activity进程,并以任何方式访问一个打开的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;
                    }
                }
            }
}

其它参考28


我认为其中一些只是框架处理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进程保留在最小数量以外的任何其他位置时间,但我可能只是偏执狂。

其它参考29


正如一些人可能已经写过的那样,关于如何关闭 Excel(对象)并不重要;同样重要的是如何打开它以及类型该项目。


在WPF应用程序中,基本相同的代码在没有或几乎没有问题的情况下工作。


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


我将所有与Excel相关的函数放入基类中,并将解析器放入子类中(不同的解析器使用常用的Excel函数)。我不想要为通用列表中的每个项目再次打开和关闭Excel,所以我只在基类中打开它一次并在子类中关闭它。将代码移动到桌面应用程序时遇到问题。我已经尝试过上面提到的许多解决方案。GC.Collect()之前已经实施过,两次建议。


然后我决定将用于打开Excel的代码移动到子类。而不是只打开一次,现在我创建一个新对象(基类)并为每个项打开Excel并在最后关闭它。性能损失,但基于几个测试Excel进程正在关闭没有问题(在调试模式下),因此临时文件也被删除。如果我将获得一些更新,我将继续测试并写一些。


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