轉:如何在託管環境下釋放COM對象

來源:互聯網
上載者:User

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進行收集時通過Compact

Heap來改變Object的位置。

為了實現COM與.Net的互動,.Net使用Wrapper技術提供了RCW(Runtime Callable Wrapper)和CCW(COM Callable

Wrapper)。.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回收。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.