转:如何在托管环境下释放COM对象

Shanny同学介绍了DataGridView数据导出到Excel的几个方法,其中讲到的使用Microsoft.Office.Core.dll即

Microsoft Office 11.0 Object Library.代码大概如下:
private void ExecuteTransfer()
{
 ApplicationClass app;
 try
 {
  app = new ApplicationClass();
  WorkBooks wbs = app.Workbooks;
  WorkBook wb = wbs.Add(XlWBATemplate.xlWBATWorksheet);
  ...
  //Use 'app' to generate Excel Object
 }
 catch
 {}
 finally
 {
  app.Quit();
  app = null;
 }

 //GC.Collect();这里Collect没有作用
}

private void DoExcecute()
{
 ExecuteTransfer();
 GC.Collect();//这里收集才有效,EXCEL.exe才能被终结。
}
Shanny说虽然在finally里释放了app,taskmanager显示被创建的EXCEL.EXE进程却没有终结。于是就在这个程序最后加了一句GC.Collect(),但是没有效果。反复测试发现必须在调用这个方法的外部使用GC.Collect()才有效。

问题:把app设置为null后,方法内部调用GC.Collect()无效,必须在方法外调用才有效?
解决:这是COM object运行在CLR下的lifetime问题。

首先对比一下COM Object与.Net Object
1.COM Object的客户必须自己管理COM Object的lifetime;.Net Object由CLR来管理(GC)
2.COM Ojbect的客户通过调用QueryInterface查询COM Object是否支持某接口并得到接口指针;.Net Object的客户使用Reflection得到Object的Description、Property和Method.
3.COM Object是通过指针引用,并且object在内存中的位置是不变的;.Net对象则可以在GC进行收集时通过CompactHeap来改变Object的位置。

为了实现COM与.Net的交互,.Net使用Wrapper技术提供了RCW(Runtime Callable Wrapper)和CCW(COM CallableWrapper)。.Net对象调用COM对象的方法时CLR就会创建一个RCW对象;COM对象调用.Net对象的方法时就会创建一个CCW对象。

一、.Net调用COM组件
RCW的主要作用:
1.RCW是Runtime生成的一个.Net类,它包装了COM组件的方法,并内部实现了对COM组件的调用
2.Marshal between .Net object and COM object.Marshal方法的参数和返回值等。如C#的string和COM的BSTR之间的转换
3.CLR为每个COM对象创建一个RCW,这与对象上的引用数无关,就是说每个COM对象只有一个RCW对象
4.RCW包含COM对象的接口指针,管理COM对象的引用计数。RCW自身的释放由GC管理。

二、COM对象的内存管理
COM对象不在托管堆里创建,也不能被GC搜索并收集。COM对象使用引用计数机制释放内存。

1.RCW作为COM对象的包装器,包含了COM对象的接口指针,并且为这个接口指针进行引用计数。RCW本身作为.Net对象是由GC管理并收集。当RCW被收集后,它的finalizer就会释放接口指针并销毁COM对象。上面的代码中,通过设置app = null;引用RCW的.Net对象就会减1,但是RCW仍然存在。这时候直接调用GC.Collect(),好像应该将RCW收集进而释放COM对象。但是为什么一定要在方法外面调用GC.Collect()呢?原因就是在ExecuteTransfer()方法中,除了app以外还有wbs,wb这些对象虽然使用的同一个COM对象,并且该COM对象只有一个RCW对象。但是,wbs和wb以及其他任何指向这个COM对象的变量都会产生对RCW的引用。在方法内部调用GC.Collect()时,我们只是设置了app = null;而其他的COM对象引用变量没有设置为空,GC自然无法收集到RCW;当方法结束时,wbs和wb等生命周期结束,他们对RCW的引用也不存在,这时候GC就可以收集RCW了,COM对象也就被释放了。
因此,我们要把代码中所有引用到COM对象(wbs,wb等等)的变量设置为null,来消除对RCW的引用,从而在方法内部就可以让GC收集到RCW,进而释放掉COM对象。

如下:
//Modified
private void ExecuteTransfer()
{
 ApplicationClass app;
 try
 {
  app = new ApplicationClass();
  WorkBooks wbs = app.Workbooks;
  WorkBook wb = wbs.Add(XlWBATemplate.xlWBATWorksheet);
  ...
  //Use app to generate Excel Object
 }
 catch
 {}
 finally
 {
  app.Quit();
  app = null;
  //消除所有对RCW的引用,否则GC.Collect()无法收集到RCW
  wbs = null;
  wb = null;
  //....其他引用到COM对象的变量设置为NULL

 }

 GC.Collect();//有效
}

2.由于GC收集时间的不确定性(由于COM对象是RCW的Finalizer执行后释放,因此即使RCW被收集了,执行Finalizer还要在另外一个线程上排队进行),这将导致COM对象在RCW被收集前滞留在内存。如果这个COM对象占用内存较大或者资源数有限(FileHandle, DBConnection),这就有可能引发内存泄漏或者程序异常。

int System.Runtime.InteropServices.Marshal.ReleaseComObject(object o)可以在GC收集RCW之前释放掉对应的COM对象.
这个方法的参数必须是引用COM对象的RCW的类型如本例中的app, wbs, wb等等.调用这个方法后,RCW就会释放接口指针,它就是一个空的Wrapper,它与COM对象的联系就断了,再对其进行调用就会Runtime Error.这时候,COM对象也就被释放了.(这是RCW的工作)Shanny也提到,即使调用了System.Runtime.InteropServices.Marshal.ReleaseComObject(app),为什么还是不能释放COM对象?原因就是wbs、wb等所有引用变量都要Release,才能释放对应的COM对象
//Modified
private void ExecuteTransfer()
{
 ApplicationClass app;
 try
 {
  app = new ApplicationClass();
  WorkBooks wbs = app.Workbooks;
  WorkBook wb = wbs.Add(XlWBATemplate.xlWBATWorksheet);
  ...
  //Use app to generate Excel Object
 }
 catch
 {}
 finally
 {
  app.Quit();
 System.Runtime.InteropServices.Marshal.ReleaseComObject(app);
 System.Runtime.InteropServices.Marshal.ReleaseComObject(wbs);
 System.Runtime.InteropServices.Marshal.ReleaseComObject(wb);
  //....release其他引用到COM对象的接口指针

 }
}
这样就避免了显式的调用GC.Collect()。

疑问:
1.RCW中的internal marshaling count和COM的reference count是两回事.调用了ReleaseComObject方法后,RCW的Internal marshaling count递减.当递减到0时,COM对象的reference count才会递减

2.ReleaseComObject()方法的返回值它的返回值是个整数,它就是RCW的internal marshaling count,当它递减为0时,COM对象的Reference Count才会递减.一般情况下RCW某个接口指针的internal marshaling count不会超过1,那么什么时候这个数值会大于1呢?MSDN是这样解释的:当指向COM的IUnknown接口的指针每次由Unmanged被marshal为managed的接口时,这个数值就会递增。

如:
ApplicationClass app = new ApplicationClass();
ODBCErrors err = app.ODBCErrors;
err = app.ODBCErrors;
err = app.ODBCErrors;//其实一般很少这样写。多线程情况下,会出现类似的情况,只是写不出例子。
...
int i = System.Runtime.InteropServices.Marshal.ReleaseComObject(err);
此时i = 2;这时就循环直到internal marshaling count为0

while(Marshal.ReleaseComObject(err)!=0);

遗留的问题:
ApplicationClass app = new ApplicationClass();
Workbooks wbs = app.Workbooks;
wbs = app.Workbooks;
wbs = app.Workbooks;
...
int i = System.Runtime.InteropServices.Marshal.ReleaseComObject(app);
int j = System.Runtime.InteropServices.Marshal.ReleaseComObject(wbs);
这时候i和j都是0;但是COM还是无法释放,也不知道这时候应该release谁?

3.ApplicationClass.Quit()这个方法到底有什么用?

附:

COM调用.Net对象
CCW的主要作用:
1.CCW实际上是runtime生成的一个COM组件,它在注册表注册,有CLSID和IID,实现了接口,内部包含了对 .NET对象的调用。
2.Marshal .NET对象与COM客户之间的调用。
3.每个.NET对象只有一个CCW,多个COM客户调用同一个CCW。
4.COM客户以指针的方式调用CCW,所以CCW分配在non-collected堆上,不受runtime管理。而.NET对象则分配在garbage-collected堆上,受runtime管理,享受CLR的种种好处。
5.CCW实际上是COM组件,所以它遵循引用计数规则。当它的引用计数为0时,会释放它对它管理的.NET对象的引用,并释放自己的内存空间。当.NET对象上引用计数为0时,则会被GC回收。

 

本文来自CSDN博客,原文:http://blog.csdn.net/lion_wing/archive/2009/08/10/4429179.aspx

你可能感兴趣的:(com)