IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Windows 10 UWP开发:支持异步的全局异常处理和堆栈信息

    汪宇杰发表于 2016-01-03 02:31:22
    love 0

    我们写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开发:报错和反馈页面的实现》把异常信息通过邮件发送给应用作者。



沪ICP备19023445号-2号
友情链接