DevExpress 的LayoutControl控制元件導致資源無法釋放的問題處理

時風拖拉機發表於2023-01-08

現象記錄

  • 前段時間同事發現我們的軟體在載入指定的外掛介面後,關閉後外掛的介面資源不能釋放, 資源管理器中不管記憶體,還是GDI物件等相關資源都不會下降。

問題程式碼

  • 問題的程式碼大概如下。
public void LoadPluginUI(string pluginID)
{
    this.Control.Clear();
    Control ctl = GetPluginUI(pluginID);// ctl是我們外掛的使用者控制元件
    this.Control.Add(ctl);
}

原因與解決方案

原因分析

解決問題的思路主要還是上windbg用sos的gcroot查引用根(這裡由於還有終結者佇列的根,實際會讓人很迷惑)。可以發現在GC控制程式碼表裡有定時器導致資源無法釋放。
在.Net下主要有4類地方會持有引用根,從而使得物件在標記階段被標記,導致GC不會回收該物件。

參考《.Net記憶體管理寶典》 第八章,垃圾回收-標記階段

  • 執行緒棧
  • GC內部根(跨代引用,類靜態變數等)
  • 終結器佇列
  • GC控制程式碼表

這裡定時器的引用根在GC控制程式碼表,透過sos可以查到定時器的週期為300ms。由於我們的程式碼裡沒有這樣的定時器,最終懷疑到DevExpress的程式碼中,透過檢查SOS的引用鏈 最終發現我們使用的DataLayoutControl有問題。
DevExpress的相關原始碼如下:

LayoutControl.cs

private void Initialize() {
SetStyle(ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.SupportsTransparentBackColor, true);
			implementorCore.InitializeComponents();
		}

ILayoutControlImplementer.cs
public virtual void InitializeComponents() {
    if(AllowCreateRootElement) InitializeRootGroupCore();
    InitializeCollections();
    InitializeTimerHandler();
    InitializeScrollerCore(as_I);
    InitializeFakeFocusContainerCore(as_I);
}
		
public virtual void InitializeTimerHandler() 
{
    if(AllowTimer) {
        this.internalTimerCore = new Timer();
        InternalTimer.Interval = 300;
        // DevExpress的LayoutControl內部使用這個定時器計算佈局等
        InternalTimer.Tick += OnTimedEvent;
        InternalTimer.Enabled = true;
    }
}
		
protected virtual bool AllowTimer {
    get { return true; }
}
		
internal void OnTimedEvent(object sender, EventArgs e) {
    if(as_I.IsUpdateLocked) return;
    as_I.ActiveHandler.OnTimer();
} 
		
public virtual void OnTimer() {
    AutoScrollByMoving();
    InvalidateHotTrackItemIfNeed();
}

  • DataLayoutControl繼承自LayoutControl,自然就有了該問題。

教訓

不用的控制元件需要主動Dispose,不能Remove就不管了。
Winform的Dispose會自動處理子控制元件,所以不論我們的使用者控制元件內部如何巢狀。直接對最外層處理Dispose就可以了。
Winform的相關程式碼如下:

protected override void Dispose(bool disposing)
{
	// 對內部維護的資源進行釋放
	ControlCollection controlCollection = (ControlCollection)Properties.GetObject(PropControlsCollection);
	if (controlCollection != null)
	{
		for (int i = 0; i < controlCollection.Count; i++)
		{
			Control control = controlCollection[i];
			control.parent = null;
			control.Dispose();
		}
		Properties.SetObject(PropControlsCollection, null);
}

解決方案程式碼

public void LoadPluginUI(string pluginID)
{
    this.Control[0].Dispose();
    Control ctl = GetPluginUI(pluginID);// ctl是我們外掛的使用者控制元件
    this.Control.Add(ctl);
}

相關文章