前言
託管資源到是好,有垃圾回收資源可以幫忙,即使需要我們的一些小小的除錯來優化,也是讓人感到欣慰的。但是非託管資源就顯得蒼白無力了,需要程式設計師自己去設計回收,同樣有設計的地方也就能體現出程式設計師的設計水平。
託管類在封裝對非託管資源的直接引用或者間接引用時,需要制定專門的規則,確保非託管資源在回收類的一個例項時釋放。
為什麼要確保呢?
是這樣子的,畫一個圖。
上圖中託管中生成並引用非託管,一但非託管和託管中的引用斷開(託管資源被回收),那麼這個時候非託管資源還在,那麼釋放這個問題就有一丟丟困難。
常見的有兩種機制來自動釋放非託管資源。
-
宣告一個構析函式作為一個類的一個成員。
-
在類中實現System.IDisposable.
好的,接下來就開始看例子吧。
正文
構析函式
先從構析函式看起吧。
class Resource
{
~Resource()
{
//釋放資源
}
}
在IL中是這樣子的。
protected override void Finalize()
{
try
{
//構析函式寫的
}
finally
{
base.Finalize();
}
}
簡單介紹一下這個Finalize 是一個終結器,我們無法重寫,文件中原文是這樣子的。
從包裝非託管資源的 SafeHandle 派生的類(推薦),或對 Object.Finalize 方法的重寫。 SafeHandle 類提供了終結器,因此你無需自行編寫。
這個SafeHandle 是啥呢?是安全控制程式碼。這東西學問很大,非該文重點,先可以理解為控制程式碼即可。
這裡簡單介紹一下控制程式碼。
職業盜圖:
再次職業盜圖:
假設有一個控制程式碼為0X00000AC6。有一個區域儲存這各個物件的地址,0X00000AC6指向這個區域裡面的區域A,A只是這個區中的一個。這個A指向真實的物件在記憶體中的位置。
這時候就有疑問了,那麼不是和指標一個樣子嗎?唯一不同的是指標的指標啊。是的,就是指標的指標。但是為啥要這麼做呢?
是這樣子的,物件在記憶體中的位置是變化的,而不是不變的。我們有時候看到電腦下面冒紅燈,這時候產生了虛擬記憶體,實際就是把硬碟當做記憶體了。但是我們發現電腦有點卡後,但是程式沒有崩潰。
當物件記憶體寫入我們的硬碟,使用的時候又讀出來了,這時候記憶體地址是變化了。這時候在記憶體中的操作是區域A的值變化了,而控制程式碼的值沒有變化,因為它指向區域A。
現在我們通過實現構析函式來實現釋放非託管資源,那麼這種方式怎麼樣呢?這種方式是存在問題的,所以現在c#的構析函式去釋放非託管談的也不多。
主要問題如下:
-
無法確認構析函式何時執行,垃圾回收機制不會馬上回收這個物件,那麼也就不會立即執行構析函式。
-
構析函式的實現會延遲該物件在記憶體中的存在時間。沒有構析函式的物件,會在垃圾回收器中一次處理從記憶體中刪除,實現構析函式的物件需要兩次。
然後所有物件的終結器是由一個執行緒來完成的,如果Finalize中存在複雜的業務操作,那麼系統效能下降是可以預見的。
實現IDisposable
看例子:
class Resource : IDisposable
{
public void Dispose()
{
//釋放資源
}
}
然後只要用完呼叫Dispose即可。
但是可能有時候程式設計師忘記主動呼叫了Dispose。
所以改成這樣。
class Resource : IDisposable
{
bool _isDisposed=false;
public void Dispose()
{
//釋放資源
_isDisposed = true;
//標誌不用掉解構函式
GC.SuppressFinalize(this);
}
~Resource()
{
if (_isDisposed)
{
return;
}
this.Dispose();
}
}
那麼是否這樣就結束了呢?
不是的。
文件中這樣介紹道:任何非密封類都應具有要實現的附加 Dispose(bool) 過載方法。
為什麼這樣說呢?因為是這樣子的,不是密封類,那麼可能會成為某個類的基類,那麼子類就要考慮基類如何釋放啊,所以加一個過載方法。
注:從終結器呼叫時,disposing 引數應為 false,從 IDisposable.Dispose 方法呼叫時應為 true。 換言之,確定情況下呼叫時為 true,而在不確定情況下呼叫時為 false。
class Resource : IDisposable
{
bool _isDisposed=false;
public void Dispose()
{
//釋放資源
Dispose(true);
//標誌不用掉解構函式
GC.SuppressFinalize(this);
}
~Resource()
{
this.Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
if (disposing)
{
//釋放託管相關資源
}
//釋放非託管資源
_isDisposed = true;
}
}
看下思路:
Dispose(bool) 方法過載
方法的主體包含兩個程式碼塊:
釋放非託管資源的塊。 無論 disposing 引數的值如何,都會執行此塊。
釋放託管資源的條件塊。 如果 disposing 的值為 true,則執行此塊。 它釋放的託管資源可包括:
實現 IDisposable 的託管物件。 可用於呼叫其 Dispose 實現(級聯釋放)的條件塊。 如果你已使用 System.Runtime.InteropServices.SafeHandle 的派生類來包裝非託管資源,則應在此處呼叫 SafeHandle.Dispose() 實現。
佔用大量記憶體或使用短缺資源的託管物件。 將大型託管物件引用分配到 null,使它們更有可能無法訪問。 相比以非確定性方式回收它們,這樣做釋放的速度更快。
那麼為什麼明確去釋放實現IDisposable 的託管資源呢?
文件中回答是這樣子的:
如果你的類擁有一個欄位或屬性,並且其型別實現 IDisposable,則包含類本身還應實現 IDisposable。 例項化 IDisposable 實現並將其儲存為例項成員的類,也負責清理。 這是為了幫助確保引用的可釋放型別可通過 Dispose 方法明確執行清理。
給個完整例子。
class Resource : IDisposable
{
bool _isDisposed=false;
private SafeHandle _safeHandle = new SafeFileHandle(IntPtr.Zero, true);
public void Dispose()
{
//釋放資源
Dispose(true);
//標誌不用掉解構函式
GC.SuppressFinalize(this);
}
~Resource()
{
this.Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
if (disposing)
{
_safeHandle?.Dispose();
//釋放託管相關資源
}
//釋放非託管資源
_isDisposed = true;
}
}
_safeHandle 和 Resource 一樣同樣可以通過構析函式去釋放非託管,但是呢,如果自己Resource 主動Dispose去釋放,那麼最好把它的子物件(託管)的Dispose給執行了,好處上面寫了。
那麼這時候為什麼在構析函式中為顯示為false呢?因為構析函式這時候本質是在終結器中執行,屬於系統那一套,有太多不確定因素了,所以乾脆_safeHandle 自己去呼叫自己解構函式。
後來我發現.net core和.net framework,他們的構析函式執行方式是不一樣的。
舉個例子:
static void Main(string[] args)
{
{
Resource resource = new Resource();
}
GC.Collect();
Console.Read();
}
在.net framework 中馬上回去呼叫構析函式,但是在.net core中並不會,等了幾分鐘沒有反應。
原因可以在:
https://github.com/dotnet/corefx/issues/5205
知道了大概怎麼回事。
好的,回到非託管中來。
那麼繼承它的子類怎麼寫呢?
class ResourceChild: Resource
{
bool _isDisposed = false;
~ResourceChild()
{
Dispose(false);
}
protected override void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
if (disposing)
{
//釋放託管相關資源
}
//釋放非託管資源
_isDisposed = true;
base.Dispose();
}
}
非託管有太多的東西了,比如說非同步dispose,using。在此肯定整理不完,後續另外一節補齊。
結
後一節,非同步。