匿名方法引起的异常
问题:最近编写一个低级的键盘钩子,用c#制作,于是用到了win32 api。但是运行大概不久后就会莫名其妙地发生异常,是非法访问内存导致的异常。
调试发现,异常的地方是不可捕获的。static class Program
{
private static HookLog hookLog;
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
hookLog = new HookLog(Form1.appPath);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
Application.Run(new Form1());
}
static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
{
hookLog.LogExceptionInfo(e.Exception);
}
}
异常发生在Application.Run(new Form1())的代码行。
提示是HookProc回调函数被释放了。于是明白是怎么一回事了。
其中,HookProc是声明的一个委托回调类型:
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
这个类型是与windows钩子的回调函输原型对应一致的。
钩子安装函数:
public void Setup()
{
if (handle != IntPtr.Zero)
return;
using(Process process = Process.GetCurrentProcess())
{
using(ProcessModule module = process.MainModule)
{
//handle = Win32Api.SetWindowsHookEx(WindowsHookTypes.WH_KEYBOARD_LL,
// new HookProc(ProcessKeyEvent), Win32Api.GetModuleHandle(module.ModuleName), // 0);
handle = Win32Api.SetWindowsHookEx(WindowsHookTypes.WH_KEYBOARD_LL,
kbdHookProc, Win32Api.GetModuleHandle(module.ModuleName), 0);
if (handle != IntPtr.Zero)
Win32Api.MessageBeep(MessageBeepTypes.IconExclamation);
}
}
}
代码中绿色注释掉的是引起异常的代码。因为直接使用了匿名委托。也就是生成的对象是个临时对象,new HookProc(ProcessKeyEvent),这个对象没有被保存。因此,对于托管代码的部分,这个委托回调函输对象将作为临时变量而存在。
但是通过调用win32 api --- SetWindowsHookEx,这个对象被传递给了非托管代码。这样就出现了问题。托管代码部分将把对象最为临时对象进行垃圾回收,因为发现没有被托管代码中的任何地方所引用。而非托管代码将仍然会调用这个回调函输,因为它无法知道托管代码已经释放了这个指针。这就是为什么运行不久后就发生一个无法捕获的越界访问异常的原因。
修改为被注释代码的下面一行代码,程序就正常了。
handle = Win32Api.SetWindowsHookEx(WindowsHookTypes.WH_KEYBOARD_LL, kbdHookProc, Win32Api.GetModuleHandle(module.ModuleName), 0);
代码中kbdHookProc是在类中声明的成员变量:
/// <summary>/// 键盘钩子回调函数
/// </summary>
private HookProc kbdHookProc;
/// <summary>
/// 构造函数
/// </summary>
public LowLevelKeboardHook()
{
disposed = false;
handle = IntPtr.Zero;
kbdHookProc = new HookProc(ProcessKeyEvent);
}
/// <summary>
/// 键盘钩子处理函数
/// </summary>
private IntPtr ProcessKeyEvent(int nCode, IntPtr wParam, IntPtr lParam)
{
KBDLLHOOKSTRUCT? rfs = Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT)) as KBDLLHOOKSTRUCT?;
if (KeyboardEvent != null && rfs.HasValue)
{
int tag = wParam.ToInt32();
if(tag == (int)WindowsMessageTypes.WM_KEYDOWN || tag == (int)WindowsMessageTypes.WM_SYSKEYDOWN)
KeyboardEvent(nCode, rfs.Value.vkCode, rfs.Value.flags);
}
return Win32Api.CallNextHookEx(handle, nCode, wParam, lParam);
}
这样,看起来没多少变化。但是用类的成员变量记住了这个回调委托对象,因此就不会被垃圾回收所回收了。
所以使用匿名函数、匿名委托、匿名临时对象等等,当涉及到非托管代码时需要万分小心。