如何正确清理Excel互操作对象?
我在C#中使用Excel interop( ApplicationClass
),并在我的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#为工作表 COM对象创建了一个包装,它没有被我的代码发布(因为我没有意识到),并且是Excel未被卸载的原因。
我在这个页面上找到了解决我的问题的解决方案,这对于C#中COM对象的使用也有一个很好的规则:
切勿在COM对象中使用两个点。
所以有了这些知识,正确的做法是:
Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);
您实际上可以干净地释放您的Excel应用程序对象,但您必须小心。
对于您访问的绝对每个COM对象维护一个命名引用的建议,然后通过Marshal.FinalReleaseComObject()
明确释放它,理论上是正确的,但不幸的是,在实践中非常难以管理。 如果有人在任何地方滑倒并使用“双点”,或者通过for each
循环或任何其他类似命令for each
单元格进行迭代,那么您将拥有未引用的COM对象并存在挂起风险。 在这种情况下,将无法找到代码中的原因; 您必须仔细检查您的所有代码,并希望找到原因,这对大型项目来说几乎是不可能完成的任务。
好消息是,您实际上不必为所使用的每个COM对象维护一个命名变量引用。 相反,调用GC.Collect()
,然后GC.WaitForPendingFinalizers()
以释放所有不包含引用的(通常较小的)对象,然后明确释放您持有指定变量引用的对象。
您还应该按照重要性相反的顺序释放命名引用:首先是范围对象,然后是工作表,工作簿,最后是Excel应用程序对象。
例如,假设你有一个名为Range对象变量xlRng
,命名工作表变量xlSheet
,命名工作簿变量xlBook
并命名为Excel应用程序变量xlApp
,那么你的清理代码可能类似于以下内容:
// 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),该工具使用终结器,以便在完成队列中提升整个对象图。 这些对象直到下一次垃圾收集才会被释放。 但是,如果您不使用VSTO,则应该只能调用一次GC.Collect()
和GC.WaitForPendingFinalizers()
。
我知道明确调用GC.Collect()
是一个GC.Collect()
当然,这两次听起来非常痛苦),但说实话,没有办法绕过它。 通过正常的操作,您将生成隐藏的对象,因此除了调用GC.Collect()
之外,您不能通过任何其他方式释放引用。
这是一个复杂的话题,但这确实就是它的全部。 一旦你为你的清理过程建立了这个模板,你就可以正常编写代码,而不需要包装器等等。:-)
我在这里有一个教程:
使用VB.Net / COM Interop自动执行Office程序
它是为VB.NET编写的,但不要被推迟,原则与使用C#时完全相同。
前言:我的答案包含两个解决方案,所以在阅读时要小心,不要错过任何东西。
关于如何使Excel实例卸载有不同的方式和建议,例如:
使用Marshal.FinalReleaseComObject()显式释放EVERY com对象(不要忘记隐式创建的com对象)。 要释放每个创建的COM对象,您可以使用这里提到的2个点的规则:
如何正确清理Excel互操作对象?
调用GC.Collect()和GC.WaitForPendingFinalizers()来使CLR释放未使用的com对象*(实际上,它可以工作,请参阅我的第二个解决方案以了解详细信息)
检查com-server-application是否显示一个消息框,等待用户回答(虽然我不确定它可以防止Excel关闭,但我几次听说过)
发送WM_CLOSE消息到主Excel窗口
在单独的AppDomain中执行与Excel一起工作的函数。 有些人认为,当AppDomain被卸载时,Excel实例将被关闭。
杀死所有在我们的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将关闭=)
链接地址: http://www.djcxy.com/p/6059.html