由C#委託回撥想到的二三事

暴走的锅巴發表於2024-11-15

寫在前面:
之前的過開發程中,我愈發覺得面對複雜的介面要求,最好還是用UserControl將不同模組的介面設計單獨封裝,以應對客戶頻繁地需求更改。這樣做能夠在面對對不同的UI要求時,動態的載入預先設計好的特定模組的UserControl,不需要用程式碼對介面進行復雜的控制,否則要用程式碼控制一個個控制元件的生成與顯示。設計之初費力,後面維護起來比較方便。
背景介紹:
最近開發新工具,針對不同的模組的資料展示我設計了不同的佈局單獨封裝為UserControl,放置在PanelControl中作為資料展示。為了能夠靈活的進行資料初始化,我給每個UserControl都訂閱了主程式的通知事件。使用委託回撥的方式,主程式中呼叫委託,子控制元件控制元件中自動執行具體的初始化方法。

//定義一個帶有一個引數的無返回值的委託
delegate void DelegateUpdateUserControl(DeviceConfig item);
//宣告委託物件
private DelegateUpdateUserControl UpdateUserControl;

大事件發生:
在測試過程中,資料的動態載入以及UserControl的切換展示沒有問題。但隨著切換次數的增加,介面變得越來越卡頓。
此時的我心中頓時警鈴大作——有些該被釋放掉的資源沒有被釋放掉?!但是每次進行介面切換,我都會呼叫Clear方法,將PanelControl裡面的物件清空(PanelControl.Clear();),這時我開始懷疑UserControl物件仍在被主程式引用,系統無法進行GC.Collect(),物件沒有被回收。
測試:
我在UserControl中的訂閱方法裡增加一行Console.WriteLine方法,進行測試。
理想情況下每切換一次UserControl,會呼叫一次委託,讓UserControl繫結的委託方法輸出一條資訊。測試卻發現,第一次會輸出1條、第二次輸出2條、第三次輸出3條...
這說明了除了第一次呼叫委託方法,後面每次呼叫都有不止一個物件在響應且數量依次增加,看來確實是每次的UserControl都沒有被成功回收。
結合.NET的垃圾回收機制,斷定UserControl物件還在被主程式引用,導致沒有被成功釋放,可鞥是沒有解除委託與方法的繫結造成的。
我將針對這個問題做了以下最佳化:
修改以下程式碼:

//在切換控制元件時,先清空原先的舊UserControl
//清空PanelControl所有控制元件,通常情況下只存在一個控制元件
PanelControlContainer.Controls.Clear();

改為:

if (PanelControlContainer.Controls.Count > 0)
{
    //獲取到UserControl(PanelControlContainer)物件
    UControlProperty control =(UControlProperty)PanelControlContainer.Controls[0];
    //取消事件掛載,解除委託繫結
    UpdateUserControl -= control.UpdateControls;
    //從PanelControl中移除舊UserControl
    PanelControlContainer.Controls.Remove(control);
    //主動釋放資源
    control.Dispose();
}

以上步驟主要是取消事件掛載,解除主程式與UserControl物件之間的引用關係,及時讓GC回收資源。
結果:
經最後測試,呼叫委託後,只會有當前最新的UserControl物件響應,介面切換時卡頓的現象消失。

擴充套件——.NET垃圾回收機制:
遇到這種情況,我們必須對.NET的垃圾回收機制有一定了解,藉此機會聊聊GC回收機制。以下內容參考《CLR via C#》(第四版)(CLR: Common Language Runtime,公共語言執行時):

  • 在C#中,當我們建立一個物件時,它的引用會被儲存在棧(Stack)中,而物件的實際資料會被儲存在堆(Heap)中。這是.NET執行時自動進行的記憶體管理,稱為垃圾回收(Garbage Collection, GC)。
  • 每個物件都有兩個開銷欄位:型別物件指標和同步塊索引,在64位應用程式中各佔8個位元組。
  • 所有引用型別的變數都稱為根物件,這些根物件是垃圾回收的起點,括區域性變數、全域性變數、靜態欄位等‌;
  • CLR開始回收時會暫停所有執行緒,,防止執行緒在CLR檢查期間訪問物件並更改其狀態;
  • 然後CLR進入GC的標記階段,CLR遍歷堆中的所有物件,將同步索引塊中的一位設為0;
  • 然後CLR檢查所有活動根,任何根引用了堆上的物件,CLR都會標記那個物件,將同步索引塊中的位設為1;
  • 一個物件被標記後,CLR會檢查那個物件的根,標記他們引用的物件,如果物件已經被標記,就不重新檢查物件的欄位,避免引起死迴圈;
  • 檢查結束後,已標記的物件至少有一個根在引用,我們說這種物件是可達的,不能被垃圾回收;
  • GC刪除未被標記的物件,把被標記的物件挪到一塊連續的空間,進行壓縮,避免空間碎片化的問題;
  • 作為壓縮的一個步驟,CLR還要從每個根減去所引用物件在記憶體中的偏移位元組數,保證每個根引用的是之前的物件;
  • 最後恢復執行之前被暫停的執行緒;

總結:
給物件事件繫結委託,釋放物件前,要主動解除委託繫結,避免出現資源無法釋放的問題出現;
CLR是基於代的垃圾回收機制,自動工作並不定時釋放不會再被訪問的資源。

相關文章