已固定的委托仍被GC回收致程序崩溃问题求助
委托被GC回收导致AutoHotkey回调崩溃解决方案
问题背景
使用AutoHotkeyEngine时,将固定后的AHKDelegate委托指针传入非托管AHK DLL作为回调,程序初始运行正常,但随机运行数小时后崩溃。事件查看器日志显示回调了已被垃圾回收(GC)的委托,尝试过封送处理和对象固定仍未解决。
相关代码
C# 代码
internal class Hotkey { [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void AHKDelegate(); private readonly AutoHotkeyEngine ahk = AutoHotkeyEngine.Instance; private readonly List<GCHandle> handles = new(); public Hotkey(AHKDelegate _onCapsLock) { SetVarOnSettings(); handles.Add(GCHandle.Alloc(_onCapsLock)); ahk.SetVar("onCapsLockPtr", GetActionDelegateStr(_onCapsLock)); ahk.ExecRaw(System.Text.Encoding.Default.GetString(Properties.Resources.CapsLock)); } ~Hotkey() { foreach(var handle in handles) { handle.Free(); } } }
非托管AHK代码
$CapsLock:: If (GetKeyState("CapsLock", "T")) { SetCapsLockState Off } else { KeyWait, CapsLock, T0.5 If (ErrorLevel) { SetCapsLockState On KeyWait, CapsLock } else { DllCall(onCapsLockPtr) } } Return
崩溃日志摘要
Application: FruitLanguageSwitcher.exe
.NET Version: 7.0.2
Message: A callback was made on a garbage collected delegate of type 'FruitLanguageSwitcher!FruitLanguageSwitcher.Core.Hotkey+AHKDelegate::Invoke'.
Exception code: 0x80131623
问题根源
- Hotkey实例被GC回收:若Hotkey对象无强引用持有,会被GC回收,其终结器释放GCHandle后,委托失去固定最终被回收,但非托管AHK代码仍持有旧指针。
- 终结器执行时机不确定:依赖终结器释放GCHandle,无法保证在非托管代码停止使用委托前不释放,存在时序风险。
- 未强引用委托本身:仅通过GCHandle固定委托,无强引用指向委托对象时,GC仍可能在特殊场景下回收它。
解决方案
1. 强引用持有委托与Hotkey实例
修改Hotkey类,添加对委托的强引用,同时实现IDisposable接口手动控制资源释放;在应用生命周期内保持Hotkey实例的强引用。
修改后的Hotkey类
internal class Hotkey : IDisposable { [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate void AHKDelegate(); private readonly AutoHotkeyEngine ahk = AutoHotkeyEngine.Instance; private readonly List<GCHandle> handles = new(); // 强引用持有委托,避免被GC回收 private readonly AHKDelegate _onCapsLock; private bool _disposed = false; public Hotkey(AHKDelegate onCapsLock) { SetVarOnSettings(); _onCapsLock = onCapsLock; // 固定被强引用持有的委托 handles.Add(GCHandle.Alloc(_onCapsLock)); ahk.SetVar("onCapsLockPtr", GetActionDelegateStr(_onCapsLock)); ahk.ExecRaw(System.Text.Encoding.Default.GetString(Properties.Resources.CapsLock)); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; // 释放GCHandle,确保委托不再被固定 foreach (var handle in handles) { if (handle.IsAllocated) { handle.Free(); } } _disposed = true; } ~Hotkey() { Dispose(false); } }
应用层保持强引用
在主应用类中声明静态字段存储Hotkey实例,确保其在程序生命周期内不会被GC回收,并在退出时手动释放:
// 主App类中 private static Hotkey _languageSwitchHotkey; protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // 创建实例并保持强引用 _languageSwitchHotkey = new Hotkey(OnCapsLockPressed); } protected override void OnExit(ExitEventArgs e) { // 程序退出时手动释放资源 _languageSwitchHotkey?.Dispose(); base.OnExit(e); } private void OnCapsLockPressed() { // 你的回调业务逻辑 }
2. 关键逻辑说明
- 强引用委托:
_onCapsLock字段确保委托对象始终有活跃引用,GC不会将其标记为可回收。 - 手动资源管理:
IDisposable接口让我们可以精准控制GCHandle的释放时机,避免终结器的不确定性。 - 长期持有Hotkey实例:静态字段
_languageSwitchHotkey保证Hotkey对象在程序运行期间不会被GC回收,其内部的委托和GCHandle一直有效。
内容的提问来源于stack exchange,提问作者Frank Xu
相关产品推荐
相关产品推荐

