CSharp - 如何正确地清理Excel interop对象

  显示原文与译文双语对照的内容

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


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

尽管这样,Excel.exe 进程仍然在后台,即使在我关闭Excel之后。 仅在我的应用程序手动关闭后才释放。

任何人都知道我做了什么,或者有一个替代的方法来确保互操作对象被正确地处理。

时间:

Excel没有退出,因为你的应用程序仍然保留对com对象的引用。

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

对我来说这是这 excelApp.Worksheets 对象没有将它的分配给一个变量,我直接用过:


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

我不知道是什么,它在内部创建的一个包装 C# 一个并不会通过我的代码( 因为我不知道它) 和被释放的缘由是为什么Excel工作表 com对象未卸载。

我这个页面,在 C# 也有好的规则使用的com对象在上对我的问题找到解决办法︰

永远不要对com对象使用 2点。


因此,使用这些知识,正确执行上述操作的方法如下:


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

你可以实际地释放你的Excel应用程序对象,但是你必须小心。

建议为你访问的每个com对象维护一个命名引用,然后通过 Marshal.FinalReleaseComObject() 显式地释放它是正确的,但不幸的是,在实践中很难管理。 如果一个曾经通过一个 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();

在完成queue,本不应该是必需的,但是,除非你使用的是用于办公( VSTO ) Visual Studio 工具,它使用终结器,从而造成整个的对象图形,以被 promoted. 这些对象将不会被释放,直到下一个垃圾收集。 但是,如果你没有使用 VSTO,你应该能够一次调用 GC.Collect()GC.WaitForPendingFinalizers()

我知道显式调用 GC.Collect() 是一个 no-no ( 而且做两次听起来很痛苦),但是没有办法绕过它。 通过正常操作,你将生成隐藏对象,你将不需要引用任何引用,因此,除了调用 GC.Collect() 之外,你还不能通过其他方法释放它们。

这是个复杂的话题,但这实际上就是其中的一个。 一旦你为清理过程建立了这个模板,你就可以正常代码了,不需要包装器,等等:

我在这里有一个教程:

自动化带有 VB.Net/COM 互操作的Office程序

它是为 VB.Net 编写的,但它不被禁用,原则与使用 C# 时完全相同。

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

我花了一些时间来尝试找出这个问题,当时XtremeVBTalk是最活跃和React最灵敏的。 下面是我的原始帖子的链接,干净地关闭了一个Excel互操作进程,即使你的应用程序崩溃了。 下面是文章的摘要,以及复制到这篇文章的代码。

  • 使用 Application.Quit()Process.Kill() 关闭互操作进程最有效,但如果应用程序崩溃,则失败。 换句话说,如果应用程序崩溃,Excel进程仍将运行松散。
  • 解决方案是让操作系统通过 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;0x2000JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE 枚举值,这个值由 MSDN 定义为:

当作业的最后一个句柄关闭时,导致与该作业关联的所有进程终止。

额外win32api调用它获取如何处理不合法( 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);

这对于我从事的一个项目是有效的:


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

it,我们才知道这就是重要的库引用每个一个Excel的com对象必须被设置为null当你有 finished. 这包括单元格,工作表,所有内容。

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

你不能执行以下操作:


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

你必须执行以下操作:


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

随后释放对象。

常见的开发人员,你的解决方案没有为我工作,所以我决定实现一个新的 trick 。

首先让我们指定"我们的目标是什么"=>"在任务管理器的任务之后不查看excel对象"?

好吧,不要挑战并开始销毁它,但是考虑不要破坏其他正在并行运行的实例操作系统 Excel 。

所以,得到电流的List 处理器和获取PID的EXCEL进程中,那么一旦你的任务完成了,我们有新的客户机进程与一个唯一的PID,发现并且消灭 List 就那一个。

<牢记任何新的excel进程在你的excel作业将被探测为新建和销毁> <> 更好的解决办法是捕获新的进程号创建excel对象,并会销毁,


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

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

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

好的编程 everyone~ ~

这里所接受的答案是正确的,但也需要注意,不仅"两点"引用需要避免,而且通过索引检索的对象也是正确的。 你也不需要等到你完成了程序来清理这些对象后,最好创建一些函数,当你完成这些对象后,可以尽快清理它们。 下面是我创建的一个函数,它分配了一个名为 xlStyleHeader的样式对象的属性:


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对象) 。

在你server,类似事件的并不是真的有必要在事后负责清理工作的标准应用程序,它做的大量工作,但在 ASP.NET 应用中,甚至如果你错过其中一种,无论你调用垃圾回收器,Excel仍然会频繁在运行。

需要很强的关注细节,而且许多。测试执行在监视著了那么一会的任务管理器在编写这段代码,但这样做不需要你为错过搜索页的代码来查找信息,一个实例。 在循环中工作时,这一点尤其重要,你需要释放对象的每个实例,即使每次循环使用相同的变量名。

...