我们写UWP应用的时候难免遇到未处理的异常,不然你的应用就会在用户面前闪退,非常没有逼格。然而Windows.UI.Xaml.Application的UnhandledException事件里面有个巨坑,就是它不能处理async异步方法里的异常。注释里也没提到这回事:
// // Summary: // Occurs when an exception can be handled by app code, as forwarded from a native-level // Windows Runtime error. Apps can mark the occurrence as handled in event data. public event UnhandledExceptionEventHandler UnhandledException;
处理全局异常确实是用这个事件没错,但是我们需要用一个线程同步的方法来搞,已经有人帮我们写好了,代码看起来逼格侧漏,无脑拷代码就行,给我们的应用增加一点逼格:
来自:https://github.com/kiwidev/WinRTExceptions
using System; using System.Threading; using Windows.UI.Xaml.Controls; namespace ShanghaiMetro.Core { /// <summary> /// Wrapper around a standard synchronization context, that catches any unhandled exceptions. /// Acts as a facade passing calls to the original SynchronizationContext /// </summary> /// <example> /// Set this up inside your App.xaml.cs file as follows: /// <code> /// protected override void OnActivated(IActivatedEventArgs args) /// { /// EnsureSyncContext(); /// ... /// } /// /// protected override void OnLaunched(LaunchActivatedEventArgs args) /// { /// EnsureSyncContext(); /// ... /// } /// /// private void EnsureSyncContext() /// { /// var exceptionHandlingSynchronizationContext = ExceptionHandlingSynchronizationContext.Register(); /// exceptionHandlingSynchronizationContext.UnhandledException += OnSynchronizationContextUnhandledException; /// } /// /// private void OnSynchronizationContextUnhandledException(object sender, UnhandledExceptionEventArgs args) /// { /// args.Handled = true; /// } /// </code> /// </example> public class ExceptionHandlingSynchronizationContext : SynchronizationContext { /// <summary> /// Registration method. Call this from OnLaunched and OnActivated inside the App.xaml.cs /// </summary> /// <returns></returns> public static ExceptionHandlingSynchronizationContext Register() { var syncContext = Current; if (syncContext == null) throw new InvalidOperationException("Ensure a synchronization context exists before calling this method."); var customSynchronizationContext = syncContext as ExceptionHandlingSynchronizationContext; if (customSynchronizationContext == null) { customSynchronizationContext = new ExceptionHandlingSynchronizationContext(syncContext); SetSynchronizationContext(customSynchronizationContext); } return customSynchronizationContext; } /// <summary> /// Links the synchronization context to the specified frame /// and ensures that it is still in use after each navigation event /// </summary> /// <param name="rootFrame"></param> /// <returns></returns> public static ExceptionHandlingSynchronizationContext RegisterForFrame(Frame rootFrame) { if (rootFrame == null) throw new ArgumentNullException(nameof(rootFrame)); var synchronizationContext = Register(); rootFrame.Navigating += (sender, args) => EnsureContext(synchronizationContext); rootFrame.Loaded += (sender, args) => EnsureContext(synchronizationContext); return synchronizationContext; } private static void EnsureContext(SynchronizationContext context) { if (Current != context) SetSynchronizationContext(context); } private readonly SynchronizationContext _syncContext; public ExceptionHandlingSynchronizationContext(SynchronizationContext syncContext) { _syncContext = syncContext; } public override SynchronizationContext CreateCopy() { return new ExceptionHandlingSynchronizationContext(_syncContext.CreateCopy()); } public override void OperationCompleted() { _syncContext.OperationCompleted(); } public override void OperationStarted() { _syncContext.OperationStarted(); } public override void Post(SendOrPostCallback d, object state) { _syncContext.Post(WrapCallback(d), state); } public override void Send(SendOrPostCallback d, object state) { _syncContext.Send(d, state); } private SendOrPostCallback WrapCallback(SendOrPostCallback sendOrPostCallback) { return state => { try { sendOrPostCallback(state); } catch (Exception ex) { if (!HandleException(ex)) throw; } }; } private bool HandleException(Exception exception) { if (UnhandledException == null) return false; var exWrapper = new UnhandledExceptionEventArgs { Exception = exception }; UnhandledException(this, exWrapper); #if DEBUG && !DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION if (System.Diagnostics.Debugger.IsAttached) System.Diagnostics.Debugger.Break(); #endif return exWrapper.Handled; } /// <summary> /// Listen to this event to catch any unhandled exceptions and allow for handling them /// so they don't crash your application /// </summary> public event EventHandler<UnhandledExceptionEventArgs> UnhandledException; } public class UnhandledExceptionEventArgs : EventArgs { public bool Handled { get; set; } public Exception Exception { get; set; } } }
现在,打开App.xaml.cs把逼装完:
public App() { this.InitializeComponent(); ... // https://github.com/kiwidev/WinRTExceptions this.UnhandledException += OnUnhandledException; }
注意这里的UnhandledExceptionEventArgs的类型是Windows.UI.Xaml.UnhandledExceptionEventArgs不是我们自定义的ShanghaiMetro.Core.UnhandledExceptionEventArgs。这里处理的是同步的情况。
private async void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e) { e.Handled = true; await new MessageDialog("Application Unhandled Exception:\r\n" + e.Exception.Message, "爆了 :(") .ShowAsync(); }
对于异步方法里的异常,需要再写个方法
/// <summary> /// Should be called from OnActivated and OnLaunched /// </summary> private void RegisterExceptionHandlingSynchronizationContext() { ExceptionHandlingSynchronizationContext .Register() .UnhandledException += SynchronizationContext_UnhandledException; } private async void SynchronizationContext_UnhandledException(object sender, UnhandledExceptionEventArgs e) { e.Handled = true; await new MessageDialog("Synchronization Context Unhandled Exception:\r\n" + e.Exception.Message, "爆了 :(") .ShowAsync(); }
然后记得在OnActivated和OnLaunched事件里把逼装完:
protected override async void OnLaunched(LaunchActivatedEventArgs e) { RegisterExceptionHandlingSynchronizationContext(); ... }
protected override async void OnActivated(IActivatedEventArgs args) { RegisterExceptionHandlingSynchronizationContext(); ... }
现在这个逼装的基本差不多了,一旦我们的应用程序有异常,不管是同步的还是异步的,都会弹一个框出来而不是闪退。
但是,为了调试方便,我们通常还需要带上堆栈信息,然而问题来了,异步的堆栈信息长这样:
来自 https://github.com/ljw1004/async-exception-stacktrace
at VB$StateMachine_3_BarAsync.MoveNext() ~~in Class1.vb:line 24~~ --- End of stack trace from previous location where exception was thrown --- at TaskAwaiter.ThrowForNonSuccess(Task task) at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at TaskAwaiter.GetResult() at VB$StateMachine_2_FooAsync.MoveNext() ~~in Class1.vb:line 19~~ --- End of stack trace from previous location where exception was thrown --- at TaskAwaiter.ThrowForNonSuccess(Task task) at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at TaskAwaiter.GetResult() at VB$StateMachine_1_TestAsync.MoveNext() ~~in Class1.vb:line 14~~ --- End of stack trace from previous location where exception was thrown --- at TaskAwaiter.ThrowForNonSuccess(Task task) at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at TaskAwaiter`1.GetResult() at VB$StateMachine_0_Button1_Click.MoveNext() ~~in Class1.vb:line 5 ~~ at VB$StateMachine_3_BarAsync.MoveNext() ~~in Class1.vb:line 24~~ --- End of stack trace from previous location where exception was thrown --- at TaskAwaiter.ThrowForNonSuccess(Task task) at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at TaskAwaiter.GetResult() at VB$StateMachine_2_FooAsync.MoveNext() ~~in Class1.vb:line 19~~ --- End of stack trace from previous location where exception was thrown --- at TaskAwaiter.ThrowForNonSuccess(Task task) at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at TaskAwaiter.GetResult() at VB$StateMachine_1_TestAsync.MoveNext() ~~in Class1.vb:line 14~~ --- End of stack trace from previous location where exception was thrown --- at TaskAwaiter.ThrowForNonSuccess(Task task) at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at TaskAwaiter`1.GetResult() at VB$StateMachine_0_Button1_Click.MoveNext() ~~in Class1.vb:line 5~~
我们希望简单粗暴明了,最好是这样:
at Test.BarAsync at Test.FooAsync()#BarAsync in Class1.vb:19 at Test.TestAsync()#FooAsync(True) in Class1.vb:14 at Test.Button1_Click() in Class1.vb:5
幸运的是,这个nuget包可以帮助我们实现这样简短的堆栈信息:https://www.nuget.org/packages/AsyncStackTraceEx/
安装之后,就可以写个小方法:
// https://github.com/ljw1004/async-exception-stacktrace private string GetExceptionDetailMessage(Exception ex) { return $"{ex.Message}\r\n{ex.StackTraceEx()}"; }
然后把刚才那两个弹框的信息给改下:
await new MessageDialog("Synchronization Context Unhandled Exception:\r\n" + GetExceptionDetailMessage(e.Exception), "爆了 :(") .ShowAsync();
现在这个逼就装完了,一旦有异常,就会看到这样的画面:
如果你希望用户反馈问题方便一点,可以结合这篇《Windows 10 UWP开发:报错和反馈页面的实现》把异常信息通过邮件发送给应用作者。