现象记录
- 前段时间同事发现我们的软件在加载指定的插件界面后,关闭后插件的界面资源不能释放, 资源管理器中不管内存,还是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); }