共计 2420 个字符,预计需要花费 7 分钟才能阅读完成。
最近遇到一个 WPF 程序发布后的启动崩溃问题。
程序在开发环境运行正常,但复制到一台 Windows 7 机器后,启动直接提示:
程序已停止工作
查看事件查看器,发现错误信息:
错误模块名称: KERNELBASE.dll
异常代码: 0xe0434352
第一眼看起来像是系统 DLL 或 WPF 兼容性问题,但继续排查后发现,真正原因完全不是 KERNELBASE.dll。
一、为什么全局异常捕获没有生效?
项目中已经添加了 WPF 常见的全局异常处理:
DispatcherUnhandledExceptionAppDomain.CurrentDomain.UnhandledExceptionTaskScheduler.UnobservedTaskException
例如:
public App()
{
DispatcherUnhandledException += OnDispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
}
但是程序启动时直接崩溃,没有任何日志。
原因是:
异常发生得比 WPF 异常机制初始化更早。
WPF 程序启动大致流程:
Main()
↓
CLR 加载程序集
↓
创建 App
↓
InitializeComponent()
↓
Run()
如果在程序集加载阶段就失败,那么:
try
{
var app = new App();
app.Run();
}
catch(Exception ex)
{
}
也不一定能够捕获。
二、增加独立启动入口
为了捕获更早阶段异常,增加自定义 Main:
[STAThread]
static void Main()
{
try
{
RunApplication();
}
catch(Exception ex)
{
StartupExceptionHandler.Handle(ex);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void RunApplication()
{
var app = new App();
app.InitializeComponent();
app.Run();
}
三、找到真正异常
增加启动日志后,终于看到真正异常:
System.IO.FileLoadException
Strong name validation failed.
失败程序集:
Prism.Container.Abstractions.dll
Prism.Container.DryIoc.dll
问题原因:
Prism 程序集强名称验证失败。
之前看到的:
KERNELBASE.dll
0xe0434352
只是 CLR 托管异常最终未处理后的系统表现,并不是根因。
四、为什么开发机正常,客户机失败?
这是发布环境中非常容易遇到的问题。
开发机和目标机器存在差异:
- 开发环境可能安装了完整开发工具
- 本机缓存和验证环境不同
- 发布目录中的依赖版本可能存在问题
因此:
“开发机能运行”并不能证明发布包没有问题。
五、解决强名称验证失败
最终定位到当前使用的 Prism 版本:
<PackageReference Include="Prism.DryIoc" Version="9.0.537" />
其中部分依赖程序集无法通过强名称验证。
调整为:
<PackageReference Include="Prism.DryIoc" Version="8.1.97" />
同时修改 Prism 命名空间:
Prism 9:
using Prism.Navigation.Regions;
Prism 8:
using Prism.Regions;
重新发布后:
Prism.dll
Prism.Wpf.dll
Prism.DryIoc.Wpf.dll
均通过:
sn.exe -vf xxx.dll
验证。
六、不要通过关闭强名称验证解决
遇到强名称验证失败,有些方案会建议:
sn.exe -Vr
跳过验证。
但是不推荐这样做。
原因:
- 修改客户机器安全配置
- 隐藏发布包问题
- 无法保证所有环境有效
正确方式应该是:
发布经过正确签名验证的程序集。
七、WPF 发布问题排查经验
这次问题总结如下:
1. KERNELBASE.dll 通常不是根因
看到:
KERNELBASE.dll
0xe0434352
不要直接认为是系统问题。
这个异常代码只能说明 CLR 托管异常最终没有被处理。
应该继续查看:
.NET Runtime Event 1026
获取真正的 CLR 异常信息。
2. 全局异常捕获需要覆盖启动阶段
普通 WPF 异常捕获只能覆盖程序运行阶段。
可靠方案应该覆盖:
CLR 加载阶段
↓
应用启动阶段
↓
UI 运行阶段
↓
后台任务阶段
只有建立更早的异常边界,才能捕获发布环境中的启动问题。
3. 启动异常处理不要依赖第三方组件
启动失败时:
- NLog 可能还没有初始化
- Prism 可能加载失败
- WPF 资源可能解析失败
因此启动异常处理最好只依赖 .NET 基础类库。
例如:
- File.WriteAllText
- Directory.CreateDirectory
- Exception.ToString()
保证在最坏情况下仍然可以留下日志。
4. 发布前检查程序集签名
对于强名称程序集,可以使用:
sn.exe -vf xxx.dll
提前验证发布目录中的程序集。
不要只依赖开发机测试结果。
总结
WPF 程序启动崩溃时,看到的错误位置往往不是实际原因。
这次问题排查过程:
KERNELBASE.dll
↓
0xe0434352
↓
.NET Runtime Event 1026
↓
FileLoadException
↓
Strong name validation failed
↓
Prism 依赖程序集签名问题
真正可靠的异常处理,不只是防止程序崩溃。
更重要的是:
当程序无法启动时,仍然能够留下足够接近根因的信息。
对于桌面应用发布来说:
日志边界必须比业务代码更早建立。
