WPF WebView2中JS回调宿主对象时C#同步调用JS脚本的死锁问题求助
解决WebView2 WPF应用中JS回调宿主方法时同步调用JS的死锁问题
我之前也遇到过完全相同的场景——在WebView2的JS回调宿主C#方法时,尝试同步调用JS脚本导致死锁。先帮你理清死锁的根源,再给出两种可行的解决方案。
死锁原因分析
默认情况下,WebView2会把JS对宿主对象的调用调度到WPF的UI线程执行。当你在这个UI线程上下文中使用Dispatcher.PushFrame同步等待JS脚本执行结果时,会触发死锁:
- UI线程被
PushFrame阻塞,无法处理WebView2的内部消息; - WebView2的JS引擎正在等待宿主方法返回结果,而宿主方法的完成依赖于新的JS脚本执行结果;
- 新的JS脚本执行完成后,WebView2需要通过UI线程的消息循环通知C#,但UI线程已经被阻塞,无法处理这个通知,最终形成循环等待。
解决方案1:将宿主对象回调调度到后台线程(推荐)
通过配置WebView2的HostObjectInvokeHandler,让JS对宿主对象的调用在后台线程执行,这样同步等待JS脚本时就不会阻塞UI线程,也不会干扰WebView2的消息处理。
步骤1:初始化WebView2时配置调用处理器
private async void InitializeWebView2() { await webView.EnsureCoreWebView2Async(); // 注册宿主对象到JS环境 var hostObject = new HostProxy(webView.CoreWebView2); webView.CoreWebView2.AddHostObjectToScript("appHost", hostObject); // 配置宿主对象调用的线程上下文:转到后台线程执行 webView.CoreWebView2.Settings.HostObjectInvokeHandler = (sender, args) => { Task.Run(() => { args.CompletedSynchronously = false; try { // 执行宿主方法并返回结果 var result = args.Invoke(); args.SetResult(result); } catch (Exception ex) { args.SetException(ex); } }); }; }
步骤2:在宿主对象中同步调用JS脚本
现在宿主方法在后台线程执行,直接同步等待ExecuteScriptAsync的结果即可,不会产生死锁:
public class HostProxy { private readonly CoreWebView2 _webView; public HostProxy(CoreWebView2 webView) { _webView = webView; } // JS会调用这个方法 public string OnSaveCallback() { // 同步执行JS脏检查脚本 var dirtyCheckResult = _webView.ExecuteScriptAsync("window.performDirtyCheck()") .GetAwaiter() .GetResult(); // 处理结果并返回给JS return dirtyCheckResult; } }
解决方案2:在UI线程上下文避免嵌套Dispatcher循环
如果不想修改宿主对象的线程上下文,可以通过ConfigureAwait(false)让JS脚本的完成回调在后台线程执行,避免阻塞UI线程的消息循环:
public class HostProxy { private readonly CoreWebView2 _webView; public HostProxy(CoreWebView2 webView) { _webView = webView; } public string OnSaveCallback() { var resetEvent = new ManualResetEventSlim(false); string scriptResult = null; Exception executionException = null; // 在UI线程发起JS脚本调用,但不让后续回调回到UI线程 Application.Current.Dispatcher.Invoke(() => { _webView.ExecuteScriptAsync("window.performDirtyCheck()") .ConfigureAwait(false) // 关键:避免回到UI线程 .ContinueWith(t => { if (t.IsFaulted) { executionException = t.Exception; } else { scriptResult = t.Result; } resetEvent.Set(); }, TaskContinuationOptions.ExecuteSynchronously); }); // 等待脚本执行完成 resetEvent.Wait(); if (executionException != null) { throw new InvalidOperationException("JS脚本执行失败", executionException); } return scriptResult; } }
注意事项
- 方案1是最优解,因为它完全避免了UI线程阻塞,同时代码逻辑更清晰;
- 如果你使用方案2,要确保
ExecuteScriptAsync的回调不会依赖UI线程的资源,否则需要额外处理线程切换; - 不要在UI线程中直接调用
ExecuteScriptAsync().Wait(),这会导致和Dispatcher.PushFrame一样的死锁问题。
内容的提问来源于stack exchange,提问作者Software Koder
相关产品推荐
相关产品推荐

