ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#跨平台上位机开发实战:从Avalonia选型到Linux部署

C#跨平台上位机开发实战:从Avalonia选型到Linux部署 做了这么多年上位机我最大的感受是以前写上位机基本默认就是“Windows WinForms/WPF”这一条路走到黑。但这两年风向明显变了——客户现场的设备越来越多跑在 Linux 上有的为了省授权费有的为了稳定性还有的是海外项目直接要求工控机不能绑定 Windows。于是“C# 能不能跨平台写上位机”这个问题成了很多同行私信问得最多的话题。先说结论能而且 C# 在这个方向的生态已经相当成熟。从 .NET Core 开始C# 早就不是 Windows 专属语言串口、Socket、Modbus 这些上位机最常用的通信能力都有跨平台实现。真正的难点不在语言而在 UI 框架和平台差异的处理上——而这恰恰是本文要拆解的核心内容。这篇文章会从方案选型、项目架构、通信模块、发布部署到常见坑位完整走一遍“一次编写多平台运行”的上位机开发路线适合正在做设备上位机、想给产品增加 Linux 支持或者准备新项目立项选型的工程师参考。1. 为什么跨平台上位机成了刚需而不是“加分项”1.1 现场环境早就变了Windows 不再是唯一选项先讲一个典型的项目场景。前年我给一家做半导体检测设备的公司做配套上位机刚开始谈需求对方工程师第一句话就是“我们设备出到客户那边系统是定制的 Linux 精简系统厂家要求上位机必须能在这个环境跑。”那台设备是一台带触屏的工控机配置不算高但完全没有 Windows 授权。如果坚持用 WinForms那就得在每台设备上额外塞一套 Windows 授权费用客户不认最后还是得做跨平台方案。这个情况在半导体、光伏、3C 自动化、汽车电子、实验室仪器这些行业里越来越普遍。以前“设备配 PC 正版 Windows”是默认前提现在客户更看重的是整机成本可控、系统越精简越好、远程运维方便、以及供应链不受单家 OS 厂商限制。甚至很多设备厂商自己为了出口海外也主动选择了 Linux 作为工控机的预装系统。做上位机的如果还只会 WinForms等于把自己锁死在一个越来越小的市场里。就算客户那边装的是 Windows也常有“Windows 7 还是 Windows 10”“32 位还是 64 位”“老机器能不能跑 .NET Framework 4.0”这类历史包袱。而跨平台方案因为底层统一运行在 .NETCore反而能把这堆兼容性问题一次性压掉。所以我越来越觉得跨平台不是“锦上添花”而是上位机开发这个工种必须补上的基本功。1.2 .NET 早已不是“Windows 专属框架”很多人的认知还停留在“.NET 就是跑在 Windows 上的”这其实是 .NET Framework 时代留下的刻板印象。从 .NET Core 1.0 开始微软就把整个运行时和基础类库做成了跨平台到了 .NET 5 之后更是直接统一成一个大版本号Windows、Linux、macOS 都能跑。你在 Linux 上用 C# 写 TCP Socket、写文件、做 JSON 序列化、跑多线程跟 Windows 上几乎没有任何差别。真正长期拖后腿的一直是 UI 层。WinForms 和 WPF 的底层直接绑死了 Win32 API天生只能在 Windows 上运行。这意味着你用它们写的界面代码到了 Linux 上就得推翻重来。换句话说如果只写通信逻辑、业务逻辑C# 跨平台早就不是问题了难点全在“界面怎么画”这一层。所以后面选方案核心就是在 UI 框架上做取舍。1.3 一次编写、多平台运行的收益到底有多大跨平台最直接的收益是维护成本。一套设备Windows 版本和 Linux 版本如果各维护一套代码每次改协议、加功能、修 Bug 都要改两遍改多了两边逻辑还会悄悄不一致。而如果只有一套核心代码只是 UI 壳子适配两个平台工作量会小一个量级。其次是界面行为的一致性。同一个版式的界面在 Windows 上测完基本逻辑到了 Linux 上不该出现“这里按钮位置不一样”“这个字体渲染不对”的问题因为渲染引擎是同一套。这一点对设备调试和售后来说非常省心——远程指导客户操作时不用先问一句“你那边是 Windows 还是 Linux”。但我也要说句实话跨平台不等于像素级完全一致。不同平台对字体、DPI、剪贴板、文件对话框的表现还是会有差异仍然需要针对平台做少量适配。理性预期是业务逻辑和界面框架 90% 可以复用剩下 10% 留给平台差异处理。这已经比维护两套代码好太多了。2. 选型没有银弹Avalonia、MAUI、Blazor Hybrid 怎么选2.1 为什么 Avalonia 最像 WPF如果你有 WPF 背景面对 Avalonia 会感觉相当亲切。Avalonia 是一个开源跨平台 UI 框架语法基于 XAML布局、样式、数据绑定、模板这些概念跟 WPF 几乎一一对应。你甚至可以把它理解为“WPF 的跨平台复刻版”但它不是简单的复制而是自己的渲染引擎Skia不依赖 Windows 的 DirectX所以能在 Linux、macOS 甚至嵌入式 Linux 上直接跑。Avalonia 的优势很明确桌面体验好适合工业上位机。表格、图表、树形控件、自定义控件都有成熟方案触屏支持也做得不错。跨平台能力强。除了 Windows/Linux/macOS还支持 ARM 架构和 Linux Framebuffer 模式嵌入式工控机没桌面环境也能直接显示界面。社区已经相当活跃。GitHub 星星多第三方控件库也在快速丰富遇到问题基本能找到答案。我在那次半导体项目里最终就选了 Avalonia。当时评估了活性和文档情况Avalonia 11 已经发布MVVM 支持和 WPF 几乎一模一样团队里写 WPF 的同事几乎零成本迁移。从结果看这个选择是对的后面写代码、调试、发布的顺畅度远超预期。2.2 MAUI 适合什么场景MAUI 是微软官方的跨平台框架可以理解为 Xamarin.Forms 的继任者。它支持 Windows、macOS、iOS、Android方向很明确移动端优先。如果你做的上位机还需要配套一个手机 App用来远程看数据、做简单控制MAUI 是个不错的选择一套 C# 代码直接覆盖桌面和移动端。但 MAUI 在 Linux 桌面这块基本是缺失的。官方没有提供 Linux 桌面支持社区维护的 Gtk 后端说实话还不适合作为生产环境依赖。如果你的主要目标是“工控机上的 Linux 桌面程序”MAUI 目前并不合适。它的定位更多是“桌面移动全端”而不是“Windows 和 Linux 双桌面”。所以我的建议是如果你的设备上位机将来要做成“桌面主控程序 手机远程 App”的组合且团队不想为了 App 单独养一套 Java/Kotlin 或 Swift 的人MAUI 值得考虑。但如果你要的是纯工控 Linux 桌面还是别折腾了。2.3 Blazor Hybrid 是另一种思路Blazor Hybrid 的思路比较特别界面用 HTML/CSS 写逻辑用 C# 写运行时把 WebView 嵌进桌面壳子里。这种方案的优点是你不需要写 XAML整个界面可以做成 Web 样式网页背景的团队可以直接上手而且同一套代码以后还能发布成真正的 Web 端灵活度很高。缺点也很现实WebView 在老旧工控机上性能表现一般启动速度比原生渲染慢包体积也比较大。另外界面渲染依赖系统 WebView 环境在精简版 Linux 系统上还得额外处理 WebView 依赖库。如果你的设备硬件配置好、团队前端资源充足、并且后续有“上位机顺便做个网页版”的想法Blazor Hybrid 是可以的。但如果你想做的是那种数据刷新频率高、操作响应极快的工业现场界面Blazor Hybrid 目前还不是最优选。2.4 给一个我自己的选择结论这套对比做下来我的选择逻辑其实很朴素先看目标平台再看团队现状最后看长期维护成本。整理成一张表格就是方案界面风格Linux 桌面支持嵌入式/无桌面上手成本适合场景Avalonia原生桌面XAML强支持 FramebufferWPF 背景几乎零成本工业上位机、工控机双平台MAUI原生控件弱无官方 Linux不支持中需要移动端配套的Blazor HybridWeb 界面一般依赖 WebView依赖 WebView前端资源充足想做 Web 版、轻量界面如果再精简成一句话做工业上位机终端主要是 Windows/Linux 桌面的首选 Avalonia要做移动端配套、且不碰 Linux 桌面可以选 MAUI公司前端资源丰富、同时规划网页版界面Blazor Hybrid 可作为长期选项。没有银弹但选型只要按照这条线走至少不会在项目中途被迫返工。3. 核心细节解析MVVM 分层与通信模块别让跨平台变成“两套代码”3.1 第一步把业务逻辑从 UI 里解放出来跨平台能不能真正做成“一次编写”关键不在 UI 框架而在架构。如果代码是“窗体里塞串口逻辑、按钮点击里直接写协议解析”这种写法换什么框架都白搭。所以我们先得把“跟平台无关的代码”和“跟展示相关的代码”分层。MVVM 在这里不是新潮概念而是非常实用的分层工具。简单理解就是View界面只在 xaml 里做数据绑定不写业务逻辑。ViewModel状态和命令持有“当前温度 25.3℃”这种属性以及“开始采集”“停止采集”这类命令。Model/Service专门做底层能力比如串口读取、Modbus 帧解析、数据库存储。这样做的最大好处是平台差异被压制在“Service”这一层。比如串口在 Windows 和 Linux 下的端口名不一样、权限体系不一样但这部分差异只发生在 SerialPort 打开的那几步到了 ViewModel 层它只在调用OpenPort(串口名)根本不知道底下是 Windows 还是 Linux。于是整个业务逻辑、界面逻辑都能复用真正每平台不同的只有很小的“适配层”。我习惯的项目分层大概是DemoApp/ Views/ # Avalonia 窗口 ViewModels/ # 界面状态和命令 Services/ # 通信服务、协议解析、配置加载 Models/ # 数据实体 Platforms/ # 平台差异适配Windows/Linux 特殊处理每次新建上位机项目都按这个结构走到了后期加新功能、加新平台都会轻松很多。3.2 串口通信在跨平台下的差异管理上位机最常碰的通信通道就是串口。C# 跨平台访问串口靠的是System.IO.Ports这个 NuGet 包它在 .NET Core 3.0 之后成了官方支持Windows 和 Linux 都能用。最基本的打开、读写、订阅数据事件两边代码完全一样using System.IO.Ports; var port new SerialPort(COM3, 115200, Parity.None, 8, StopBits.One); port.DataReceived (s, e) { var data new byte[port.BytesToRead]; port.Read(data, 0, data.Length); // 处理收到的数据 }; port.Open();但平台差异藏得很深最多的问题集中在这几个地方。第一是端口名。Windows 下是COM3、COM7这种Linux 下是/dev/ttyUSB0USB 转串口或/dev/ttyS0主板原生串口。好的做法是把端口名做成配置并且提供“自动枚举”能力。Windows 直接用SerialPort.GetPortNames()就能列出 COM 口Linux 下面则要扫描 /dev 目录public static Liststring GetAvailablePorts() { if (OperatingSystem.IsWindows()) { return SerialPort.GetPortNames().ToList(); } else { var names new Liststring(); foreach (var pattern in new[] { /dev/ttyUSB*, /dev/ttyS*, /dev/ttyACM* }) { names.AddRange(Directory.GetFiles(/dev, pattern).Select(Path.GetFileName)); } return names.Distinct().ToList(); } }我把这部分独立成SerialPortHelper界面下拉框里加载哪个平台就调用对应实现对 ViewModel 完全透明。第二是权限。Linux 下打开串口经常报UnauthorizedAccessException原因很简单当前用户不在dialout或uucp用户组里没有访问串口设备的权限。解决办法是在目标机器上执行sudo usermod -aG dialout 当前用户名然后注销重新登录。如果是在产品里分发最好在部署文档里明确写明这一步不然到客户现场才发现串口打不开会很尴尬。第三是串口打开超时和丢数据。Linux 下 USB 转串口芯片质量参差不齐常见的 CH340/CP2102 在某些内核版本上会有延迟。建议串口属性设置两方面ReceivedBytesThreshold 1保证有数据就立刻触发事件ReadTimeout和WriteTimeout不要设成无限等待避免异常时卡死线程。3.3 Modbus 等工业协议的跨平台注意点工业现场大量使用 Modbus RTU / Modbus TCP 协议。C# 这边比较常用的库是老牌的 NModbus4但我要提醒一句NModbus4 年久失修它最后一个版本存在一个已知的 RTU 从站 Bug并且上游基本不维护了。建议使用它的新维护分支 NModbus或者干脆自己在通信层上写轻量协议解析。我用 NModbus 做 Modbus TCP 主站读取寄存器的示例using Modbus.Device; using var client new TcpClient(192.168.1.10, 502); var master ModbusIpMaster.CreateIp(client); // 读从站地址为 1 的设备起始寄存器地址 0连续读 10 个保持寄存器 ushort startAddress 0; ushort numRegisters 10; ushort[] registers master.ReadHoldingRegisters(1, startAddress, numRegisters); Console.WriteLine($寄存器0值: {registers[0]});Modbus 协议本身是纯 C# 实现的 TCP/串口操作不依赖底层平台所以这段代码在 Windows 和 Linux 上表现一致。但有一点要注意Modbus 主站轮询时一般建议在后台线程里定时读取不要在 UI 线程里 sleep 轮询。这也是上位机开发里的一个通用原则——UI 线程只负责界面所有周期性的采集动作放进Task.Run或独立的工作线程。如果设备不是标准 Modbus而是一套私有协议建议自己写一个IProtocol.cs接口把帧组装、校验、解析放在独立类里。这样以后换设备、换通信方式上层代码基本不用动。跨平台项目的架构设计核心就是把这种“变化点”封到接口后面。3.4 多线程与 UI 线程跨平台框架的“潜台词”多线程在上位机里绕不开因为采集数据往往是并发持续的。但跨平台框架里最容易踩的坑就是线程问题。Avalonia、MAUI 这些框架都遵循 UI 线程模型只有 UI 线程能安全地修改界面控件后台线程改了要么报错要么行为不确定。Avalonia 里更新 UI 的统一姿势是Dispatcher.UIThread.Post(() { TemperatureText ${value:F2} ℃; });对应的 ViewModel 属性要实现INotifyPropertyChanged或者用CommunityToolkit.Mvvm的源生成器public partial class MainViewModel : ObservableObject { [ObservableProperty] private string temperatureText -- ℃; [ObservableProperty] private bool isRunning; }这样在后台线程收到数据后Dispatcher.UIThread.Post切回 UI 线程再赋值TemperatureText界面就能自动刷新。这个模式在 WinForms、WPF、Avalonia 里思想一样只是 API 名字略有区别。我建议所有上位机项目都把“数据接收后更新界面”的路径统一走这个模式不要在多个地方各写一套否则后期排查界面不刷新会非常痛苦。4. 实操过程与核心环节实现从空项目到双平台发布4.1 环境搭建与项目创建我以 Avalonia 为例从头走一遍完整操作。前置条件先装好 .NET SDK建议直接上 .NET 8 LTS。然后安装 Avalonia 项目模板dotnet new install Avalonia.Templates dotnet new avalonia.mvvm -n DemoApp cd DemoApp模板生成的工程自带 App.axaml、MainWindow.axaml、ViewLocator.cs 以及 ViewModel 示例结构很清爽。接着加入需要用到的包dotnet add package Avalonia.Desktop dotnet add package Avalonia.Themes.Fluent dotnet add package CommunityToolkit.Mvvm dotnet add package System.IO.Ports dotnet add package NModbus如果项目中要用 LiveCharts2 画实时趋势曲线还可以加dotnet add package LiveChartsCore.SkiaSharpView.Avalonia这里说明一下System.IO.Ports为什么要单独安装它在 .NET Core 3.0 之后是独立 NuGet 包不在默认的共享框架里。不装的话代码里 usingSystem.IO.Ports会直接报错。4.2 一个最简单的数据采集示例假设我们要做一个温度采集上位机从串口读一个温湿度传感器。先定义数据模型public class SensorData { public double Temperature { get; set; } public double Humidity { get; set; } public DateTime Timestamp { get; set; } }再定义 ViewModel绑定串口配置和实时显示public partial class MainViewModel : ObservableObject { [ObservableProperty] private string portName COM3; [ObservableProperty] private int baudRate 115200; [ObservableProperty] private string temperatureText -- ℃; [ObservableProperty] private string humidityText -- %; [ObservableProperty] private bool isRunning; private SerialPort? _port; [RelayCommand] private void Start() { if (OperatingSystem.IsWindows()) { _port new SerialPort(PortName, BaudRate, Parity.None, 8, StopBits.One); } else { // Linux 下端口名类似 /dev/ttyUSB0 _port new SerialPort(PortName, BaudRate, Parity.None, 8, StopBits.One); } _port.DataReceived OnDataReceived; _port.Open(); IsRunning true; } private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { // 这里简化实际要按协议拆帧解析 byte[] buffer new byte[_port!.BytesToRead]; _port.Read(buffer, 0, buffer.Length); double temp ParseTemperature(buffer); Dispatcher.UIThread.Post(() { TemperatureText ${temp:F2} ℃; }); } private double ParseTemperature(byte[] buffer) { // 真实项目中是协议解析这里直接占位 return 25.0; } [RelayCommand] private void Stop() { if (_port ! null _port.IsOpen) { _port.DataReceived - OnDataReceived; _port.Close(); _port.Dispose(); _port null; } IsRunning false; } }界面部分StackPanel Margin20 Spacing12 TextBlock Text串口配置 FontWeightBold/ Grid ColumnDefinitions*,Auto ColumnSpacing8 TextBox Text{Binding PortName} Watermark端口号如 COM3 或 /dev/ttyUSB0/ TextBox Grid.Column1 Text{Binding BaudRate} Watermark波特率 Width120/ /Grid StackPanel OrientationHorizontal Spacing12 Button Content启动采集 Command{Binding StartCommand}/ Button Content停止采集 Command{Binding StopCommand}/ /StackPanel TextBlock Text{Binding TemperatureText} FontSize28 FontWeightBold/ TextBlock Text{Binding HumidityText} FontSize28 FontWeightBold/ /StackPanel这样一个最简化的上位机雏形就出来了。整个工程里业务逻辑全部在 ViewModelAvalonia 只负责展示。后面换串口实现、换网络通信都不影响界面层。4.3 一键发布到 Windows 和 Linux开发环境里调试没问题后发布就是很关键的一步。Avalonia 的发布方式跟 .NET 一致。我的经验是直接给每个目标平台指定 Runtime Identifier。Windows x64dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFiletrueLinux x64dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFiletrue这样每个平台都能得到一个单文件可执行程序。--self-contained true的意思是把 .NET 运行时也打进去目标机器不需要预装 .NET这在上位机现场非常实用——客户机器一般很干净不会替你装好运行时。不过有一点要提醒PublishSingleFiletrue和剪裁Trim不是一回事。剪裁可以进一步减小体积但上位机这种反射多、动态加载多的场景剪裁很容易把运行时需要的程序集裁掉导致程序启动就崩。所以我的建议是发布时保持默认不开启 Trim先保证稳定再考虑体积。单文件体积大点也就几十 MB对工控机来说完全不是问题。发布完成后Linux 下运行还需要一些系统库。如果目标机是带桌面的 Ubuntu/Debian通常直接能跑如果是精简系统可能需要手动装sudo apt update sudo apt install libfontconfig1 libice6 libsm6 libx11-6 libxrandr2 libxinerama1 libdbus-1-3其中libfontconfig1关系到字体渲染最容易缺缺了会报错或界面字形异常。建议打包一个部署脚本把依赖安装和串口权限设置一起解决。4.4 ARM 平台树莓派和嵌入式工控机怎么办除了 x64嵌入式场景经常遇到 ARM 平台。.NET 发布 ARM64 版很简单dotnet publish -c Release -r linux-arm64 --self-contained trueAvalonia 在 ARM Linux 上也能跑但有两个问题要提前规划。一是无桌面环境。很多嵌入式工控机上根本没有 X11/WaylandAvalonia 有专门的 Linux Framebuffer 模式可以不依赖桌面直接渲染到屏幕上。实现方式是写一个自定义的 AppBuilder 扩展加载Avalonia.LinuxFramebuffer包然后配置连接 /dev/fb0 之类的显示设备。这个方案在需要原生全屏界面的触摸屏设备上很好用。二是性能和触摸优化。ARM 设备性能参差不齐界面布局尽量简单避免大量阴影、模糊等特效触摸屏的话要做大按钮、大间距防止误触。我在树莓派 4B 上跑过一个仪表盘界面CPU 占用控制在 20% 以内完全可接受。所以“Windows Linux x64 Linux ARM”这套组合用 C# 和 Avalonia 是能真正落地的。5. 常见问题与排查技巧实录5.1 串口打不开、端口列表里看不到设备这个是我遇到最多的问题没有之一。排查步骤整理成一张表现象可能原因排查/解决Windows 下 GetPortNames 没有设备驱动没装 / USB 线接触不良设备管理器查看是否识别到 COM 口重新插拔并安装芯片厂商驱动Linux 下没有 /dev/ttyUSB0USB 转串口芯片未识别lsusb、dmesgLinux 下能枚举但打开报权限错误用户不在 dialout 组sudo usermod -aG dialout 用户名重新登录Linux 下打开不报错但收不到数据串口配置不符 / 设备未供电用 minicom 或 python serial 先单独测试链路这里特别想说的一点是遇到串口问题不要急着在上位机代码里反复试先拿一个成熟的串口调试工具Windows 用串口助手Linux 用 minicom直接测试通讯链路。链路本身通不通、参数对不对先用工具确认。否则你会在自己的代码里排除半天 bug最后发现是传感器那边波特率设错了。5.2 DllImport 调用本地库失败找 SO 文件找不到上位机经常要对接厂家提供的 DLL常见的有相机 SDK、运动控制卡、采集卡等。Windows 下你把 DLL 放到运行目录就行但 Linux 下 P/Invoke 的规则不同默认会优先找libxxx.so所以Windows 下写DllImport(xxx.dll)Linux 下要写DllImport(libxxx.so)如果你的代码里硬编码了 DLL 名跨平台就会非常痛苦。我建议用运行时动态加载的方式按平台拼接库名再加载string libName OperatingSystem.IsWindows() ? vendor_sdk.dll : libvendor_sdk.so;另外Linux 下.so文件还要能被系统加载器找到常见解决方法是把 .so 文件放到/usr/local/lib然后执行sudo ldconfig或者在程序启动时先切换当前目录到 .so 所在目录。最实用的一招是找厂家要 Linux 版 SDK并要求提供对应的 .so 和头文件Windows DLL 和 Linux SO 一般不是一回事。有些厂家甚至只提供 Windows SDK这种项目在 Linux 下的方案就要单独评估了。5.3 Avalonia 界面中文变成方块这是刚从 Windows 迁到 Linux 时最容易出现的视觉问题。Windows 系统自带微软雅黑、宋体Linux 默认字体不一定覆盖中文结果界面上所有中文都变成一个个方块“□□□”。解决方法很简单目标机器安装中文字体sudo apt install fonts-noto-cjk如果在搞嵌入式精简系统也可以把字体文件放在程序资源里用 Avalonia 的 FontManager 直接加载内置字体。两种方案我都用过前者适合普通工控机后者适合不想依赖系统字体的场景。后者实现稍复杂但一旦做好界面在所有机器上字形一致视觉更稳定。5.4 窗口尺寸、坐标不对界面上出现黑边这个问题多发生在高 DPI 或不同分辨率切换的触摸屏设备上。Avalonia 的跨平台窗口管理在不同平台行为有细微差别尤其是从 Windows 高分屏切到 Linux 普通屏时窗口可能超出屏幕或者 DPI 缩放不正确。遇到这种情况我的经验是从两个地方入手。一是检查系统缩放Linux 桌面/XWayland 的缩放配置可能不一致二是在 Program.cs 的 AppBuilder 里显式配置平台选项禁用不稳定的特性.With(new Win32PlatformOptions { UseDeferredRendering false }) .With(new X11PlatformOptions { UseDeferredRendering false })虽然这不一定是所有黑边的万能解但在老显卡、远程桌面环境下实测明显更稳。另外工控程序建议直接做全屏或最大化运行减少窗口管理器带来的干扰。5.5 发布后程序启动崩溃体积小到不正常如果你图省事把PublishTrimmedtrue开了大概率会遇到启动崩溃或者某个功能运行时才报TypeLoadException、FileNotFoundException。这是因为上位机程序大量使用反射、动态加载剪裁器无法准确分析需要保留的程序集把必要的类型裁掉了。排查方法是在运行时设置环境变量打开剪裁日志export DOTNET_EnableWriteXorExecute0 export DOTNET_EnableDiagnostics1或者直接用框架依赖发布不剪裁、不自包含把体积问题交给磁盘——工控机动辄几十 GB 存储真不缺一个几十 MB 的程序。所以我强烈建议上位机项目不要轻易开 Trim。性能瓶颈通常不在程序体积而在通信链路和设备端没必要为了省那点空间给自己埋雷。5.6 UI 不刷新数据变了界面不动这个通常是后台线程直接改了绑定属性但界面没收到通知。跨平台 UI 框架对线程模型卡得比 WinForms 还严解决套路还是那句所有界面状态更新统一走 Dispatcher。而且要注意 ViewModel 属性是否实现了通知接口。CommunityToolkit.Mvvm的[ObservableProperty]会自动生成通知这是最省事的写法。如果用了源生成器但还是没刷新检查一下类是不是partial命名空间对不对属性是不是私有字段——这类问题编译期不报错运行起来特别隐蔽。排查这类问题我有个笨但有效的方法在属性 setter 里临时加一行日志打印调用线程 ID。日志显示不是 UI 线程就直接锁定是线程问题日志根本不输出那就是绑定或通知没生效。用二分法缩小范围比对着代码猜快得多。最后聊点实际的感受这套跨平台方案走下来我最深的体会是跨平台不是把代码复制一份改改就能成功的而是从一开始就要把平台相关的东西隔离起来。串口端口名、权限体系、动态库命名、字体、DPI、发布参数每一个乍看都是小问题放到一起就是劝退级别的拦路虎。但反过来说这些坑也都有标准解法只要架构上留好了接口踩过一次之后就再也不会踩了。如果让我重新做一次我的第一步不会是急着写界面而是先到目标 Linux 系统上跑一个空白 Avalonia 窗口把运行库、字体、串口权限这些问题先全部扫平再开始填充业务功能。环境问题前置处理后面就能把注意力全部集中在协议和逻辑上。这个方法我后来在每个新项目里都会用省掉了大量现场调试时间。
返回列表