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回收。