使用WinMM.dll实现高精度定时器如何避免GC回收引发的崩溃?
崩溃根因
你遇到的崩溃不是GC阻塞定时器导致的,核心原因是传递给WinMM非托管接口的回调委托被GC回收了:
你在StartElapsedTimer方法中创建的TimerEventHandler委托是局部变量,方法执行完成后没有任何托管引用指向该委托,GC运行时会直接将其标记为可回收对象,而WinMM的非托管逻辑还持有该委托的指针,触发回调时就会抛出CallbackOnCollectedDelegate错误。
另外你传入的myData是方法栈上的局部变量,方法执行完成后栈内存会被释放,非托管代码访问该地址也会引发未知异常。
修复方案
只需要延长回调委托和用户上下文变量的生命周期,和WinMMWrapper实例保持一致即可,修改步骤如下:
- 在
WinMMWrapper类中新增两个私有字段,存储回调委托和用户上下文 - 构造函数中提前初始化回调委托
- 修改
StartElapsedTimer方法,使用预存的字段作为参数传入非托管接口
修改后的核心代码
public class WinMMWrapper : IDisposable { [DllImport("WinMM.dll", SetLastError = true)] public static extern uint timeSetEvent(int msDelay, int msResolution, TimerEventHandler handler, ref int userCtx, int eventType); [DllImport("Winmm.dll", CharSet = CharSet.Auto)] static extern uint timeKillEvent(uint uTimerID); public delegate void TimerEventHandler(uint id, uint msg, ref int userCtx, int rsv1, int rsv2); public enum TimerEventType { OneTime = 0, Repeating = 1, } private readonly Action _elapsedAction; private readonly int _elapsedMs; private readonly int _resolutionMs; private readonly TimerEventType _timerEventType; private uint _timerId; private bool _disposed; // 新增字段:存储回调委托,生命周期和实例一致 private TimerEventHandler _tickHandler; // 新增字段:存储用户上下文,避免栈内存释放 private int _userCtx; public WinMMWrapper(int elapsedMs, int resolutionMs, TimerEventType timerEventType, Action elapsedAction) { _elapsedMs = elapsedMs; _resolutionMs = resolutionMs; _timerEventType = timerEventType; _elapsedAction = elapsedAction; // 提前初始化委托,存在实例字段中不会被GC回收 _tickHandler = TickHandler; _userCtx = 1; } public bool StartElapsedTimer() { StopTimer(); // 直接传入预存的委托和用户上下文字段,不要创建局部委托 _timerId = timeSetEvent(_elapsedMs, _resolutionMs / 10, _tickHandler, ref _userCtx, (int)_timerEventType); return _timerId != 0; } // 其余代码保持不变 public void StopTimer() { if (_timerId != 0) { timeKillEvent(_timerId); _timerId = 0; } } private void TickHandler(uint id, uint msg, ref int userctx, int rsv1, int rsv2) { _elapsedAction(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (!_disposed && disposing) StopTimer(); _disposed = true; } ~WinMMWrapper() { Dispose(false); } }
注意事项
- 你的
WinMMWrapper实例存储在静态变量Global.timer中,静态变量属于GC根引用,只要不主动置为null,实例和其持有的委托字段就不会被回收,完全满足定时器长期运行的要求 - 不需要使用定时器时,要主动调用
Global.timer.Dispose()方法释放非托管的定时器资源,避免内存泄漏
内容的提问来源于stack exchange,提问作者Ozgur Saklanmaz
相关产品推荐
相关产品推荐

