设置NavigationCacheMode后导航返回触发CallWindowProc栈溢出异常
问题根源
当你把NavigationCacheMode设为Enabled或Required后,页面实例会被缓存起来,导航离开再返回时不会重新创建。但你的MainPage_Loaded事件每次页面加载(包括从缓存返回)都会执行,每次执行都会调用SetWindowLongPtr替换窗口过程——这就导致窗口过程被一层一层Hook:新的Hook指向你的HotKeyProc,而上一次的Hook过程被存在origHotKeyProc里。等窗口消息触发时,HotKeyProc调用CallWindowProc到上一个Hook,那个Hook又会调用它自己的CallWindowProc,无限套娃直接把栈撑爆,就出现StackOverflowException了。
另外还有两个潜在坑:
- 没在页面卸载时清理Hook和热键注册,就算不缓存,多次导航也会重复Hook;
- 委托
hotKeyProcD可能被GC回收,导致调用无效指针,触发类似你提到的ExecutionEngineException。
解决方案
核心思路
- 窗口是全局的(整个应用只有一个主窗口),Hook逻辑只需要执行一次,不能每次页面加载都重复操作;
- 必须在合适的时机清理注册的热键和Hook的窗口过程,避免内存泄漏和无效调用。
修改后的代码示例
MainPage.xaml.cs 修改
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using System; using System.Runtime.InteropServices; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.Input.KeyboardAndMouse; using Windows.Win32.UI.WindowsAndMessaging; namespace NavigationProc; public sealed partial class MainPage : Page { // 用静态变量标记是否已经Hook过,避免重复操作 private static bool _isWndProcHooked = false; private static WNDPROC? _origHotKeyProc; private static WNDPROC? _hotKeyProcD; private static HWND _mainWindowHandle; public MainPage() { InitializeComponent(); Loaded += MainPage_Loaded; } private static LRESULT HotKeyProc(HWND hWnd, uint Msg, WPARAM wParam, LPARAM lParam) { const uint WM_HOTKEY = 0x0312; if (Msg == WM_HOTKEY) { // 从全局窗口里找到当前的MainPage实例 if (App.MainWindow.Content is Frame frame && frame.Content is MainPage mainPage) { if (mainPage.ToggleButton.IsEnabled) { mainPage.ToggleButton.IsChecked = !mainPage.ToggleButton.IsChecked; } } } return PInvoke.CallWindowProc(_origHotKeyProc, hWnd, Msg, wParam, lParam); } private void MainPage_Loaded(object sender, RoutedEventArgs e) { if (_isWndProcHooked) return; // 获取并保存主窗口句柄 _mainWindowHandle = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow)); // 注册F6热键 const int hotKeyId = 0x0000; _ = PInvoke.RegisterHotKey(_mainWindowHandle, hotKeyId, HOT_KEY_MODIFIERS.MOD_NOREPEAT, 0x75); // 只Hook一次窗口过程 _hotKeyProcD = HotKeyProc; IntPtr hotKeyProcPtr = Marshal.GetFunctionPointerForDelegate(_hotKeyProcD); IntPtr wndPtr = PInvoke.SetWindowLongPtr(_mainWindowHandle, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyProcPtr); _origHotKeyProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(wndPtr); _isWndProcHooked = true; } // 提供静态方法,用于窗口关闭时清理资源 public static void CleanupHotKeyAndHook() { if (!_isWndProcHooked) return; // 注销热键 const int hotKeyId = 0x0000; _ = PInvoke.UnregisterHotKey(_mainWindowHandle, hotKeyId); // 恢复原始窗口过程 if (_origHotKeyProc != null) { IntPtr origProcPtr = Marshal.GetFunctionPointerForDelegate(_origHotKeyProc); _ = PInvoke.SetWindowLongPtr(_mainWindowHandle, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, origProcPtr); } _isWndProcHooked = false; _hotKeyProcD = null; _origHotKeyProc = null; } private void Button_Click(object sender, RoutedEventArgs e) { _ = Frame.Navigate(typeof(OtherPage)); } }
在App.xaml.cs中添加窗口关闭时的清理逻辑
using Microsoft.UI.Xaml; namespace NavigationProc; public partial class App : Application { public static MainWindow MainWindow { get; private set; } public App() { InitializeComponent(); } protected override void OnLaunched(LaunchActivatedEventArgs args) { MainWindow = new MainWindow(); MainWindow.Closed += MainWindow_Closed; MainWindow.Activate(); } private void MainWindow_Closed(object sender, WindowEventArgs args) { // 窗口关闭时清理热键和Hook MainPage.CleanupHotKeyAndHook(); } }
关键说明
- 用静态变量标记Hook状态,确保窗口过程只被替换一次,彻底避免多层Hook导致的递归;
- 热键处理中通过全局窗口实例查找页面控件,适配页面缓存的场景;
- 静态委托
_hotKeyProcD不会被GC回收,解决了潜在的ExecutionEngineException问题; - 窗口关闭时清理资源,避免内存泄漏和无效指针调用。
内容的提问来源于stack exchange,提问作者Ryan Luu
相关产品推荐
相关产品推荐

