System.Windows.Forms.Timer 内存泄漏问题剖析

System.Windows.Forms.Timer 内存泄漏

如果窗口中使用了 Timer 成员, 什么情况下造成内存泄漏?

  • 使用 Form.Show() 方法时,修改了设计器的代码如下?
 protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                // 注释掉了这条语句。
                // components.Dispose();
            }
            base.Dispose(disposing);
        }
  • 使用 Form.ShowDialog() 方法,未使用 using 指定窗口的实例 或 未显示调用 Dispose 方法造成内存泄漏。

踩坑的场景

我们有位资深工程师,设计了这么一个交互设计:

  • 存在一个主窗口,主窗口中有一个按钮1。
  • 点击按钮1时弹出一个 Form.ShowDialog() 对话框1。
  • 话框1中使用 Tiemr 定时读取数据库,刷新 UI 页面。(Form 的对话框没使用 using 关键,未显示调用 Dispose)
  • 查看完成,关闭对话框。

看似完成简单的 UI 操作,但是由于 Timer 未释放,导致了 Timer 的定时任务一直累计。
如果一直操作下去,那么 UI 会越来越卡(这位大神没有异步),Timer 执行任务时是切换到 UI 线程上下文执行的。
同时多个同步的数据库查询或更改任务一直执行,导致了数据库超负荷CPU占用高。

原理分析

Timer 的创建过程

public Timer(IContainer container)
   : this()
  {
   if (container == null)
   {
    throw new ArgumentNullException("container");
   }

   // 这里添加到容器中,然后在窗口的 IDispose 中释放掉 Timer。
   container.Add(this);
  }

Timer.Enable 创建了非托管资源(罪魁祸首)

[SRCategory("CatBehavior")]
  [DefaultValue(false)]
  [SRDescription("TimerEnabledDescr")]
  public virtual bool Enabled
  {
   get
   {
    if (timerWindow == null)
    {
     return enabled;
    }
    return timerWindow.IsTimerRunning;
   }
   set
   {
    lock (syncObj)
    {
     if (enabled == value)
     {
      return;
     }
     enabled = value;
     if (base.DesignMode)
     {
      return;
     }
     if (value)
     {
      if (timerWindow == null)
      {
       timerWindow = new TimerNativeWindow(this);
      }

      // 这里为当前实例申请 GCHandle,它保护对象不被垃圾回收。 当不再需要 GCHandle 时,必须通过 Free() 将其释放。
      // 罪魁祸首。
      timerRoot = GCHandle.Alloc(this);
      timerWindow.StartTimer(interval);
      return;
     }
     if (timerWindow != null)
     {
      timerWindow.StopTimer();
     }
     if (timerRoot.IsAllocated)
     {
      timerRoot.Free();
     }
    }
   }
  }

那么既然是非托管的,在哪里调用了我们的委托呢?

winform 封装了一个 TimerNativeWindow 类型.


TimerNativeWindow(Timer timer)
{
    _owner = timer;
}

// 在窗口过程里调用了 OnTick
protected override void WndProc(ref Message m)
   {
    if (m.Msg == 275)
    {
     if ((int)(long)m.WParam == _timerID)
     {
        // 
      _owner.OnTick(EventArgs.Empty);
      return;
     }
    }
    else if (m.Msg == 16)
    {
     StopTimer(destroyHwnd: true, m.HWnd);
     return;
    }
    base.WndProc(ref m);
   }

而 Timer 类是这么定义 Tick 的。

  [SRCategory("CatBehavior")]
  [SRDescription("TimerTimerDescr")]
  public event EventHandler Tick
  {
   add
   {
    onTimer = (EventHandler)Delegate.Combine(onTimer, value);
   }
   remove
   {
    onTimer = (EventHandler)Delegate.Remove(onTimer, value);
   }
  }
        
  protected virtual void OnTick(EventArgs e)
  {
   if (onTimer != null)
   {
    onTimer(this, e);
   }
  }

ShowDialog 时资源未释放的原因

调用 Form.ShowDialog() 来显示窗口,以下方法均没有释放 Timer 的非托管资源,故造成了内存泄漏。

public DialogResult ShowDialog()
  {
   return ShowDialog(null);
  }

  public DialogResult ShowDialog(IWin32Window owner)
  {
   if (owner == this)
   {
    throw new ArgumentException(SR.GetString("OwnsSelfOrOwner", "showDialog"), "owner");
   }
   if (base.Visible)
   {
    throw new InvalidOperationException(SR.GetString("ShowDialogOnVisible", "showDialog"));
   }
   if (!base.Enabled)
   {
    throw new InvalidOperationException(SR.GetString("ShowDialogOnDisabled", "showDialog"));
   }
   if (!TopLevel)
   {
    throw new InvalidOperationException(SR.GetString("ShowDialogOnNonTopLevel", "showDialog"));
   }
   if (Modal)
   {
    throw new InvalidOperationException(SR.GetString("ShowDialogOnModal", "showDialog"));
   }
   if (!SystemInformation.UserInteractive)
   {
    throw new InvalidOperationException(SR.GetString("CantShowModalOnNonInteractive"));
   }
   if (owner != null && ((int)UnsafeNativeMethods.GetWindowLong(new HandleRef(owner, Control.GetSafeHandle(owner)), -20) & 8) == 0 && owner is Control)
   {
    owner = ((Control)owner).TopLevelControlInternal;
   }
   CalledOnLoad = false;
   CalledMakeVisible = false;
   CloseReason = CloseReason.None;
   IntPtr capture = UnsafeNativeMethods.GetCapture();
   if (capture != IntPtr.Zero)
   {
    UnsafeNativeMethods.SendMessage(new HandleRef(null, capture), 31, IntPtr.Zero, IntPtr.Zero);
    SafeNativeMethods.ReleaseCapture();
   }
   IntPtr intPtr = UnsafeNativeMethods.GetActiveWindow();
   IntPtr intPtr2 = ((owner == null) ? intPtr : Control.GetSafeHandle(owner));
   IntPtr zero = IntPtr.Zero;
   base.Properties.SetObject(PropDialogOwner, owner);
   Form ownerInternal = OwnerInternal;
   if (owner is Form && owner != ownerInternal)
   {
    Owner = (Form)owner;
   }
   try
   {
    SetState(32, value: true);
    dialogResult = DialogResult.None;
    CreateControl();
    if (intPtr2 != IntPtr.Zero && intPtr2 != base.Handle)
    {
     if (UnsafeNativeMethods.GetWindowLong(new HandleRef(owner, intPtr2), -8) == base.Handle)
     {
      throw new ArgumentException(SR.GetString("OwnsSelfOrOwner", "showDialog"), "owner");
     }
     zero = UnsafeNativeMethods.GetWindowLong(new HandleRef(this, base.Handle), -8);
     UnsafeNativeMethods.SetWindowLong(new HandleRef(this, base.Handle), -8, new HandleRef(owner, intPtr2));
    }
    try
    {
     if (dialogResult == DialogResult.None)
     {
        // 这里进行对话框的消息循环。
      Application.RunDialog(this);
     }
    }
    finally
    {
     if (!UnsafeNativeMethods.IsWindow(new HandleRef(null, intPtr)))
     {
      intPtr = intPtr2;
     }
     if (UnsafeNativeMethods.IsWindow(new HandleRef(null, intPtr)) && SafeNativeMethods.IsWindowVisible(new HandleRef(null, intPtr)))
     {
      UnsafeNativeMethods.SetActiveWindow(new HandleRef(null, intPtr));
     }
     else if (UnsafeNativeMethods.IsWindow(new HandleRef(null, intPtr2)) && SafeNativeMethods.IsWindowVisible(new HandleRef(null, intPtr2)))
     {
      UnsafeNativeMethods.SetActiveWindow(new HandleRef(null, intPtr2));
     }
     SetVisibleCore(value: false);
     if (base.IsHandleCreated)
     {
      if (OwnerInternal != null && OwnerInternal.IsMdiContainer)
      {
       OwnerInternal.Invalidate(invalidateChildren: true);
       OwnerInternal.Update();
      }

      // 这里没有释放非托管资源。
      DestroyHandle();
     }
     SetState(32, value: false);
    }
   }
   finally
   {
    Owner = ownerInternal;
    base.Properties.SetObject(PropDialogOwner, null);
   }
   return DialogResult;
  }

为什么 Form.Show 就能释放呢?

// 这是编译器自动帮我们创建的 *.Designer.cs 的代码。
 protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                
                components.Dispose();
            }
            base.Dispose(disposing);
        }

窗口关闭时会自动调用 Dispose 方法,如果我们注释掉 components.Dispose(),那么同样也会造成内存泄漏。

posted @ 2023-02-14 01:03  RafaelLxf  阅读(365)  评论(0)    收藏  举报