软件崩溃错误收集和查看
WinUI 3作为一个新式桌面应用框架,还有很多方面需要打磨,崩溃错误也是频发的。
在使用IDE调试(Debug模式)过程中,异常大都会被编译器捕捉并被我们看见,也就是几乎不需要我们手动收集。
但一旦进入生产环境(Release模式)时,就会出现这些问题:
- 异常和崩溃不会被调试器(Debugger)捕捉,也不会有任何输出。
- 生产环境中我们追求稳定,就会抑制一些不是很重要的异常。但这并不代表异常就解决了,它反而可能到其他地方表现了出来,而且甚至表现为崩溃,这导致我们找不到故障的原发地而一头雾水。
- AOT模式下大量代码被剪裁。在WinUI 3 AOT技术预览版中,调试模式下的错误甚至都不会被调试器捕捉,这迫使我们手动输出错误。
此时如果我们能获取到异常的原因、错误的堆栈,将极大帮助我们改进代码。
由此,我们需要输出错误日志以便定位问题所在。
定位并得到异常对象
想要输出异常信息,首先要获取到异常信息。
以下几个位置可以帮助我们捕获到想要的异常:
DebugSettings.BindingFailed和DebugSettings.XamlResourceReferenceFailed
这两个事件是调试模式下,XAML中出现的绑定错误和资源应用错误,一般都不是崩溃性错误。
这个在一般情况下都可以在调试器中看到,但万一错过了,手动记录这个仍然可以帮助到我们。
它们的参数中没有异常信息,都是通过第二个参数的Message
属性获取。
在调试模式下,如果没有预定义DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT
符号,则自动生成的代码会帮我们把DebugSettings.BindingFailed
的信息输出到“输出”窗口中。
在调试模式下,如果没有预定义DISABLE_XAML_GENERATED_RESOURCE_REFERENCE_DEBUG_OUTPUT
符号,则自动生成的代码会帮我们把DebugSettings.XamlResourceReferenceFailed
的信息输出到“输出”窗口中。
// 注:此处假定Logger具有void LogWarning(string, Exception?)、void LogError(string, Exception?)、void LogCritical(string, Exception?)方法
DebugSettings.BindingFailed += (o, e) =>
{
Logger.LogWarning(e.Message, null);
};
DebugSettings.XamlResourceReferenceFailed += (o, e) =>
{
Logger.LogWarning(e.Message, null);
};
Application.UnhandledException
这个事件是Application
的实例成员,当出现了不导致崩溃又未处理的异常时,它会被触发。
在调试模式下,如果没有预定义DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION
符号,则自动生成的代码会帮我们把异常信息输出到“输出”窗口中(不包含堆栈)。
所以我们可以手动记录并让它触发调试器断点。
Application.Current.UnhandledException += (o, e) =>
{
Logger.LogError(e.Message, e.Exception);
e.Handled = true;
#if DEBUG
if (Debugger.IsAttached)
Debugger.Break();
#endif
};
以上API是WinUI 3框架提供的异常事件,下面的则是.NET SDK自带的了:
TaskScheduler.UnobservedTaskException
这个事件是指,如果我们不await
异步方法(不返回(即void
类型)或返回的Task
等类型被抛弃),则主线程无法获取这些异步方法里出现的异常,它们会被这个事件转发出来。
由于不await
的异步方法广泛存在,所以这个事件十分常用。和之前不同的是,为了不要让记录的异常影响过大,我们需要手动设置它已被观测:
TaskScheduler.UnobservedTaskException +=` (o, e) =>
{
Logger.LogError(nameof(TaskScheduler.UnobservedTaskException), e.Exception);
e.SetObserved();
#if DEBUG
if (Debugger.IsAttached)
Debugger.Break();
#endif
};
AppDomain.UnhandledException
这个事件是AppDomain
的实例成员。由于Application.UnhandledException存在,大量普通的异常都不会到达这个事件。
所以剩下会到这个事件的都是一些崩溃性错误(尤其是IsTerminating
为true
时),而崩溃性错误多是和框架相关的COMException
,是分析时的重难点。
AppDomain.CurrentDomain.UnhandledException += (o, e) =>
{
if (e.IsTerminating)
Logger.LogCritical(nameof(AppDomain.UnhandledException), e.ExceptionObject as Exception);
else
Logger.LogError(nameof(AppDomain.UnhandledException), e.ExceptionObject as Exception);
#if DEBUG
if (Debugger.IsAttached)
Debugger.Break();
if (e.IsTerminating && Debugger.IsAttached)
Debugger.Break();
#endif
};
Logger的编写要点
在可视化窗口应用中,一般不包含用来输出的控制台,所以我们需要输出到文件中。
而官方的日志库居然没有直接的文件日志,让人十分纠结。
如果我们决定要自己实现日志类时,一定要注意递归地输出异常的所有内部异常,不然难以获取真正的有效信息。
一些常见异常
对于一些常见、却又信息很少的异常,我们应该做到熟记于心,或者至少能想到那个方面,以免多次浪费大量时间:
DepenecyProperty赋值时的异常
在一些看起来根本不会出错的分支上的错误,常常隐含着体系性、根本性的编程错误。
例如,给控件DepenecyProperty
赋值时出现问题,大多数是从非UI线程访问了UI线程的错误,此时需要用Control.DispatcherQueue.TryEnqueue()
来送回主线程执行。
关于它的原理,我在另一篇文章中进行了详细论述,此处不再展开。
XAML解析失败
以下的异常信息也是经常会遇到的:
Microsoft.UI.Xaml.Markup.XamlParseException: 'XAML parsing failed.'
在WinUI 3还在预览版时这种问题十分普遍,而且没有具体位置和相关消息,十分恼人。
但现在大部分问题都会在编译时报错了,没能报错的也会有大概位置和具体为什么失败的原因,定位问题已经容易多了。
如果真的遇到这种问题,主要应该考虑那些以字符串形式构造其他类型对象的语句,例如:
<AppBarButton Icon="Accept" />
此处Accept
会被转换为SymbolIcon
并显示。
如果此处将Accept
替换为未定义的字符串,则会抛出XamlParseException
。
好在现在这个问题已经会被明确指出出错的位置和内容,可以很快定位到问题位置。
若还是在其他地方遇到了没有具体报错和位置的XamlParseException
,可以尝试二分法注释掉一半的XAML文件,以此快速定位到出错的语句。