尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

WPF智慧工厂数据平台实战:架构设计、MVVM与Prism落地经验

WPF智慧工厂数据平台实战:架构设计、MVVM与Prism落地经验 做WPF的智慧工厂数据平台很多人的第一反应是“不就是做几个界面绑定一下数据吗”真扎进去才发现完全不是那么回事。我前后接手过两三个产线数据平台项目从最开始拿WinForms凑合到彻底转向WPF再到现在整套框架基于Prism来搭中间踩过的坑、推倒重来的代码足够写一本小册子了。这篇东西我不打算给你讲那种“从入门到精通”的教科书内容而是以一个实际交付的WPF智慧工厂数据平台为蓝本把架构怎么设计、设计模式怎么落地、控件有哪些高频坑、设备数据怎么接、项目怎么打包一条线全部捋清楚。这套内容适合两类人一类是刚被安排做工厂数据平台、上位机项目的.NET开发另一类是已经在用WPF写界面、但总觉得项目越写越乱想引入MVVM和模块化架构改善维护性的朋友。看完你至少能回答三个问题项目骨架应该怎么搭MVVM和Prism在真实项目里到底怎么用以及哪些地方的坑是我用交付时间换来的。1. WPF智慧工厂数据平台的整体拆解与技术选型1.1 先搞清楚这个平台到底要解决什么问题智慧工厂数据平台这个名字听起来很大但落到实际项目里核心就三件事把设备的数据采集上来把生产的状态展示出来把异常的信息推送出去。再往细了说通常包含设备实时状态监控、产量统计、质量数据追溯、报警记录、工艺参数曲线、视觉检测结果展示这些模块。它本质是一个数据汇集和可视化中心连接的下游是PLC、传感器、数据库、视觉系统连接的上游是一块块大屏幕和操作员的电脑。我做过的项目中最典型的一个车间有几十台设备每台设备通过Modbus TCP或者OPC UA上报数据另外还有几套工业相机做产品外观检测。原来现场是每台设备配一个工控机各自为政数据靠人工抄表汇总。平台的目标就是把这些零散的设备和数据统一接到一个客户端里实时刷新、历史可查、异常能报警。搞清楚这一点非常重要因为技术选型和管理层的预期、现场运维人员的使用习惯强相关别一上来就追求技术上的炫酷稳定、好维护、操作员能快速上手才是第一位的。1.2 为什么选择WPF而不是Web或者WinForms很多领导会问为什么不做成网页版浏览器打开就能看多方便。我做的时候也认真评估过。Web方案的优势在部署和跨终端访问但工厂车间这个环境里有几个现实问题绕不开现场工控机配置参差不齐浏览器渲染大型实时数据图表容易卡顿操作员需要的是类似传统组态软件那种高频刷新、键盘快捷键响应极快的交互还需要直接访问本机的串口、网口、USB相机等硬件资源浏览器在这一块天生受限。而且很多工厂内网环境并不允许随意部署Web服务数据安全的要求也倾向于数据留在本地处理。至于WinForms我早年写了不少它开发效率确实高但到了需要做复杂数据模板、动画状态、自定义样式、多分辨率适配的时候WinForms的GDI绘制模式会让你怀疑人生。WPF的矢量渲染、数据绑定、控件模板、样式系统在构建这种数据密集、状态多变的工业客户端时优势是代差级的。当然WPF也有学习曲线和性能陷阱但对我来说综合权衡下来WPF是这个场景最合适的选择。1.3 技术栈清单与选型理由这里直接给出我实际使用的技术栈给准备动工的你一个参考技术项使用方案选型理由开发框架.NET 6 / .NET 8WPF跨平台不是刚需LTS版本长期维护性能比.NET Framework提升明显MVVM框架Prism 8模块化、Region导航、依赖注入、命令系统、事件聚合器一套全齐UI控件库HandyControl 自研样式开箱即用的工业风格控件主题可以统一定制图表控件OxyPlot免费、轻量、支持实时曲线适合大量数据点刷新场景日志组件Serilog结构化日志滚动文件配合调试和现场问题定位数据库SQLite本地缓存 SQL Server历史数据现场解析度配置SQLite做实时缓存和离线补传设备通信Modbus TCP、OPC UA、S7协议根据现场设备自由选择基于简单工厂模式动态创建部署打包Advanced Installer处理依赖、服务注册、数据库脚本执行比较省心这套组合我在两个项目里完整跑通过踩坑少、社区资料多遇到问题能找到人问。控件库我没有选择完全商业化的大厂产品主要是项目预算有限而且HandyControl的源码开放遇到不满意的样式可以直接改模板这在工业现场是刚需因为甲方审美差异极大经常要微调整体风格。2. 设计模式在WPF客户端项目里的实际落地2.1 MVVM是WPF的第一设计模式说WPF绕不开MVVM这不是因为它时尚而是WPF本身的数据绑定和命令系统就是为MVVM准备的。MVVM的核心是三层View负责界面呈现ViewModel负责界面状态和行为逻辑Model负责业务数据和数据访问。它的本质是把界面开发从“事件驱动直接操作控件”变成“数据驱动自动同步”让View和业务逻辑解耦。实际项目里我见过很多半吊子MVVM最典型的问题是ViewModel里塞了几百行业务逻辑或者反过来View的CodeBehind里还是有大量的事件处理代码。我的习惯是View里只放跟视觉呈现相关的东西比如动画触发、窗口行为控制凡是能通过绑定解决的一律走数据绑定凡是用户点击后要处理的一律走Command。这样做的直接好处是界面需求变了业务代码不用动数据来源换了界面也不用动。另一个容易忽略的点是MVVM不意味着完全没有CodeBehind在有些场合比如窗口关闭前确认、拖拽文件进来写少量事件代码是合理的别为了教条把自己逼疯。2.2 用Prism落地模块化架构项目一大了把所有Window和ViewModel堆在一个项目里必死我第一版就是这么死的。Prism的优势在于它把模块化、依赖注入、导航、事件聚合这些架构级能力都集成好了你不需要自己造轮子。按照Prism的思路我把智慧工厂数据平台拆成了几个模块工程Platform.Core核心公共库包含基类、接口、通用转换器、日志封装Platform.Modules.Dashboard总览大屏模块Platform.Modules.DeviceMonitor实时设备监控模块Platform.Modules.Quality质量数据模块包含视觉检测结果展示Platform.Modules.History历史数据查询与报表模块Platform.Shell主宿主程序负责加载模块和配置区域模块化的好处不光是代码好看更重要的是团队协作。不同人负责不同模块互不干扰集成的时候只要约定好接口就行。模块的注册很简单在模块类里实现IModule接口在RegisterTypes里注册服务在OnInitialized里注册Region导航public class DeviceMonitorModule : IModule { public void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterForNavigationDeviceMonitorView, DeviceMonitorViewModel(); } public void OnInitialized(IContainerProvider containerProvider) { var regionManager containerProvider.ResolveIRegionManager(); regionManager.RegisterViewWithRegion(MainContentRegion, typeof(DeviceMonitorView)); } }模块和模块之间通过Region进行界面组合通过事件聚合器进行通信通过接口进行数据交互这样就把强耦合变成了弱耦合。这块投入的时间值回票价后面每次加功能都特别有底气。2.3 简单工厂模式管理设备驱动与数据源设备通信是工厂数据平台里最杂的一部分。现场可能有不同品牌的PLC有的走Modbus TCP有的走OPC UA有的走S7协议。如果代码里到处写if-else去判断设备类型一旦新增一种设备就要改动所有相关地方。这里我用简单工厂模式做了一个设备驱动工厂把创建对象和具体使用逻辑分离。定义统一的设备驱动接口public interface IDeviceDriver { string DeviceId { get; } bool Connect(); void Disconnect(); DeviceData Read(); bool IsConnected { get; } }然后创建一个工厂类根据配置的设备类型和协议返回对应的驱动实例public static class DeviceDriverFactory { public static IDeviceDriver Create(DeviceConfig config) { return config.Protocol switch { ModbusTcp new ModbusTcpDriver(config.DeviceId, config.IpAddress, config.Port), OpcUa new OpcUaDriver(config.DeviceId, config.EndpointUrl), S7 new S7Driver(config.DeviceId, config.IpAddress, config.Rack, config.Slot), _ throw new NotSupportedException($不支持的协议: {config.Protocol}) }; } }这样新增设备协议时只需要新增一个实现IDeviceDriver的类在工厂里加一个case分支其它业务代码完全不用动。这就是开闭原则的落地。另外在UI层我用了一个DeviceService作为门面屏蔽了工厂和驱动细节ViewModel只和DeviceService打交道。这套设计在后期扩展新设备时省了很多事属于越用越香的那种。2.4 事件聚合器解决跨页面联动工业场景有一个常见的需求设备报警时不管当前在哪个页面都要弹报警提示、状态栏变红、同时写入日志。如果通过ViewModel之间互相引用来做代码会乱成蜘蛛网。Prism提供的事件聚合器就是专门干这个的实际上它就是观察者模式的一种实现。发布和订阅的事件类public class DeviceAlarmEvent : PubSubEventDeviceAlarmInfo { }订阅方public class MainWindowViewModel : BindableBase { private readonly IEventAggregator _eventAggregator; public MainWindowViewModel(IEventAggregator eventAggregator) { _eventAggregator eventAggregator; _eventAggregator.GetEventDeviceAlarmEvent().Subscribe(OnDeviceAlarm); } private void OnDeviceAlarm(DeviceAlarmInfo alarmInfo) { // 更新状态栏、弹窗提示等 } }发布方_eventAggregator.GetEventDeviceAlarmEvent().Publish(new DeviceAlarmInfo { DeviceId M001, Message 温度超限 });需要注意两个坑。第一个是订阅后忘记退订会造成内存泄漏Prism虽然跟随View生存期有些自动处理但长生命周期对象之间通信还是要养成在销毁时Unsubscribe的习惯。第二个坑是事件在哪个线程发布订阅回调就在哪个线程执行如果设备采集线程发布了事件订阅方法里去更新UI控件就会碰线程问题。我的做法是订阅方法里统一用Dispatcher调度或者使用Prism的ThreadOption.UIThread。这两个坑我都踩过现场调试时最难受。3. 界面层搭建控件库、高频控件坑点与性能优化3.1 HandyControl与自研样式结合的落地经验工业软件在界面风格上两极分化一种是传统的深灰色工控风格另一种是领导喜欢的科技感大屏风格。HandyControl的优势是提供了大量基础控件和主题配置换肤只需要改资源字典不用动业务页面。我在项目中用HandyControl作为底子配合自定义的DeepBlue主题做出来效果比较接近组态软件的感觉。具体使用上只要在App.xaml里引入HandyControl的资源字典再给窗口加上对应的样式即可。比如用HandyControl的Window样式和按钮样式Application.Resources ResourceDictionary ResourceDictionary.MergedDictionaries ResourceDictionary Sourcepack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml/ ResourceDictionary Sourcepack://application:,,,/HandyControl;component/Themes/Theme.xaml/ /ResourceDictionary.MergedDictionaries /ResourceDictionary /Application.Resources我的建议是不要所有东西都依赖第三方控件库核心业务控件要能做模板定制。比如状态指示灯、设备卡片这种高频组件我直接封装成UserControl内部状态用依赖属性暴露这样数据绑定非常方便换样式也只改一处。另外引入HandyControl后要注意版本兼容不同版本的HandyControl和Prism组合偶发样式资源加载顺序问题基本排查思路是检查App.xaml里资源字典的合并顺序。3.2 DataGrid列内容过长时如何完整显示设备名称、报警描述这类字段经常很长DataGrid默认的显示方式是截断操作员看不到完整内容体验很差。搜索WPF相关内容时这个问题也是高频痛点。我这里有三种方案各有适用场景。第一种是给DataGridTextColumn设置ElementStyle用TextBlock的TextTrimming加上ToolTip来显示完整内容DataGridTextColumn Header报警描述 Binding{Binding AlarmMessage} Width* DataGridTextColumn.ElementStyle Style TargetTypeTextBlock Setter PropertyTextTrimming ValueCharacterEllipsis/ Setter PropertyToolTip Value{Binding Text, RelativeSource{RelativeSource Self}}/ /Style /DataGridTextColumn.ElementStyle /DataGridTextColumnToolTip绑定Text自身鼠标悬停时就能看到完整内容。但这个方案有个问题ToolTip不是自绘的文本很长时显示的框很难看我一般会做全局的ToolTip模板限制最大宽度并支持换行。第二种是使用DataGridTemplateColumn在CellTemplate里自定义TextBlock可以做更多控制DataGridTemplateColumn Header报警描述 Width* DataGridTemplateColumn.CellTemplate DataTemplate TextBlock Text{Binding AlarmMessage} TextTrimmingCharacterEllipsis ToolTip{Binding AlarmMessage} ToolTipService.InitialShowDelay200/ /DataTemplate /DataGridTemplateColumn.CellTemplate /DataGridTemplateColumn第三种是启用ToolTip的附加属性把ToolTip的Style写成全局资源所有列统一生效。注意DataGrid开启虚拟化时ToolTip的上下文是DataContext不要用Tag之类的东西去传上下文很容易丢。直接绑定当前行的属性是最稳妥的。3.3 NumericUpDown数据验证与错误提示工业参数设置界面到处都是数值输入比如温度上限、节拍时间、设备参数。HandyControl提供了NumericUpDown控件但数据验证和错误提示要自己搭好。我的做法是让ViewModel实现INotifyDataErrorInfo接口再在绑定上设置ValidatesOnNotifyDataErrors和NotifyOnValidationError。ViewModel验证属性示例public class DeviceParameterViewModel : BindableBase, INotifyDataErrorInfo { private decimal _maxTemperature; public decimal MaxTemperature { get _maxTemperature; set { SetProperty(ref _maxTemperature, value); ValidateMaxTemperature(value); } } private void ValidateMaxTemperature(decimal value) { if (value 0 || value 350) { AddError(nameof(MaxTemperature), 温度上限必须在0到350之间); } else { RemoveError(nameof(MaxTemperature)); } } }XAML里就这样绑定hc:NumericUpDown Value{Binding MaxTemperature, ValidatesOnNotifyDataErrorsTrue, UpdateSourceTriggerPropertyChanged} Minimum0 Maximum999 Increment1/HandyControl会在验证失败时给周边控件加上红框同时可以利用控件自带的ToolTip或者模板显示错误信息。这个方案的关键点是UpdateSourceTrigger设置为PropertyChanged数据源在输入过程中就能及时反馈而不是失焦才验证。如果用了IDataErrorInfo要注意索引器里对每个属性名的判断否则一个属性报错会导致所有绑定都触发验证。NumericUpDown输入非法字符时不要在ViewModel里做字符串转换让控件先拦住ViewModel只处理合法数值。3.4 用OxyPlot实现实时趋势曲线实时曲线是工厂数据平台的高频需求温度趋势、产能统计、压力曲线。我对比过LiveCharts和OxyPlot最终选OxyPlot主要是它在数据量大的场景下表现稳定而且是纯C#实现MIT协议可以放心商用。OxyPlot在WPF里用起来很顺手创建一个PlotModel添加LineSeries然后启动一个定时器或者订阅数据源把数据点加进Seriesprivate void OnNewData(DeviceData data) { _tempPointList.Add(DateTimeAxis.CreateDataPoint(data.Timestamp, data.Temperature)); if (_tempPointList.Count MaxPointCount) { _tempPointList.RemoveAt(0); } _tempSeries.Points.Clear(); foreach (var pt in _tempPointList) { _tempSeries.Points.Add(pt); } _plotModel.InvalidatePlot(true); }实时刷新最大的坑是性能。每分钟几万个点全量塞进图表界面必卡。我常用的优化手段有三个。一是限制显示点数保留最近一段时间窗口的数据比如展示最近30分钟超过的自动移除。二是降低刷新频率UI上的曲线不需要每个数据点都刷新可以每500毫秒批量刷新一次。三是用LineSeries的性能模式开启InterpolationAlgorithm为null关闭抗锯齿大数据点场景下折线绘制速度能快很多。另外OxyPlot的坐标轴刻度自适应也是开销大户如果数据窗口固定可以考虑手动固定Axis的Minimum和Maximum避免反复计算。4. 数据采集层设备对接、视觉集成与性能优化4.1 设备数据采集服务如何设计与线程调度平台能不能用关键看数据采集层稳不稳。我习惯把采集服务抽象成一个独立于UI的后台服务用Channel或者简单的事件把数据推给ViewModel。现场最常碰到的设备是PLCModbus TCP读取寄存器这个操作有几种方式轮询、订阅变更、中断触发。工业现场最常用的还是轮询因为简单、可控、各种PLC都支持。轮询间隔要根据设备允许的读取频率来定太快会给PLC造成负担太慢又会让监控画面看起来“卡顿”我一般用500毫秒到1秒。采集服务的核心逻辑是把耗时的IO操作放到后台线程然后把结果通过SynchronizationContext或者Dispatcher切回UI线程。用async/await时要注意采集服务构造函数里不要直接启动任务命周期不对会导致重复启动。我一般让采集服务实现一个Start()和Stop()方法在主窗口Loaded里Start在Closed里Stop。我用一个比较简单可靠的模式后台Task持续轮询拿到数据后扔进Channel队列UI侧有专门的消费者订阅并更新界面。这样即使偶发数据量大也不会阻塞UI线程队列还有削峰填谷的作用。这个方案实测下来比直接在每个采集回调里Invoke UI稳定很多特别是设备数量多、刷新频率高的场景。4.2 VisionMaster二次开发与WPF界面集成近几年工厂里视觉检测几乎是标配海康VisionMaster是使用率很高的视觉平台。WPF上位机要对接VisionMaster不是简单地调用一个接口就行。我的经验是分几步走先引用VisionMaster安装目录下提供的二次开发程序集不同版本命名略有差异核心是可以加载vmpro工程方案的SDK然后在代码中初始化运行时环境加载方案文件.sol或.vmproj再设置输入图像触发流程执行最后获取检测结果。VisionMaster的二次开发接口整体风格偏向C风格但提供了C#接口绝大多数常用操作能覆盖。需要注意几个实际问题引用版本必须跟现场安装的VisionMaster版本一致否则运行时会因为程序集版本不匹配暴雷。图像源要区分相机硬触发采集还是本地图片读取WPF界面集成时如果是硬触发注意相机回调线程和UI线程的切换。检测结果的获取一般是通过获取流程的“结果输出”节点数据比如OK/NG、坐标、测量值。这些值拿到后要封装成自定义类再绑定到WPF的数据表格里。如果需要在WPF界面里嵌显示图像或检测结果可以用WindowsFormsHost承载VisionMaster的图像控件或者把检测结果绘制成图片后用WPF的Image控件显示。前者集成简单但WPF叠加科技感样式会比较受限后者灵活但需要自己处理绘制逻辑。这块的调试比较磨人尤其是现场图像光源不稳定导致检测结果频繁变化时需要同时看ViewModel里的原始数据和界面上的绑定值我的建议是把VisionMaster返回的原始结果序列化成Json保存到日志方便对比分析。4.3 大数据量加载与界面流畅度优化历史数据查询这个模块很容易爆卡。一个车间跑一个月历史记录有上百万条如果直接把DataTable绑给DataGrid那UI必然崩溃。我在做历史数据页面时的处理办法是分页加载加UI虚拟化。粗暴的分页方式是数据库端分页每页只查100条翻页再查。DataGrid开启虚拟化后滚动时的性能问题基本能解决。但要注意WPF的DataGrid默认开启的虚拟化是行方向如果列特别多列方向也要考虑性能。数据绑定集合的刷新我不用普通ObservableCollection一条条添加而是批量添加后用DeferRefresh来抑制中间的刷新否则每Add一条都会触发UI更新。另一个容易忽视的性能瓶颈是数据绑定里使用了复杂的IValueConverter每绑定一个单元格就执行一次转换器上万行下来开销巨大。我的原则是历史数据表格里尽量减少Converter能用属性直接绑定就在ViewModel里算好时间格式、状态文本这类转换在ViewModel层面处理完再暴露给View。还有DataGrid的样式尽量不要层层嵌套禁用的动画效果这些都会影响大列表的滚动帧率。5. 部署打包与项目维护的实战心得5.1 WPF项目的打包方式怎么选项目交付的时候打包部署经常被当成小事实际上现场环境千奇百怪打包环节出了问题会非常影响验收。WPF项目常用的打包方案有InstallShield、Advanced Installer、Visual Studio Installer Projects、MSIX、ClickOnce。我的经验是工业现场客户端推荐用Advanced Installer或者InstallShield这类桌面安装包工具因为它们可以处理.NET运行时安装、数据库脚本、服务注册、文件夹权限、桌面快捷方式这些乱七八糟的需求。我目前的打包习惯是分成几个步骤用Visual Studio生成Release版本目标平台选x64如果对接的VisionMaster或者Native驱动是32位则要选x86前提是提前确认好。在Advanced Installer里创建项目添加主输出、依赖项、运行库。配置数据库连接字符串到配置文件安装时允许用户修改。加上日志目录和数据库文件的处理比如安装目录下的Data文件夹写权限。做升级包策略一般用高级安装工具自带的更新机制或者自己写一个检查更新的小工具。打包时有个坑比较隐蔽WPF项目如果使用了自定义字体、第三方Native库、C运行库这些文件不会自动出现在发布目录里需要手动在打包工具里添加。我就遇到过一次部署到现场后按钮图标全变方块排查半天发现是字体文件没被复制进去。5.2 上线后常见的部署与运行问题交付上线不等于完事现场跑起来问题更多。我列几个高频问题当速查表问题现象可能原因排查与解决部署后双击没反应缺少.NET运行时、缺少VC依赖查Windows事件日志安装对应运行时检查目标平台位数配置文件路径不一致安装目录与开发目录结构不同使用相对路径配置初始化时自动创建设备连接超时防火墙拦截了Modbus/OPC UA端口加防火墙例外规则检查交换机配置触摸屏点击漂移未做系统DPI适配WPF应用设置PerMonitorV2 DPI感知调整布局数据图表不刷新定时器被GC回收持有定时器强引用避免局部变量定时器被回收启动时闪退全局未捕获异常在App里加DispatcherUnhandledException和TaskScheduler.UnobservedTaskException全局处理上线阶段我建议把Serilog的日志级别调到Debug把设备通信的原始报文记录到滚动文件。现场问题多数情况下通过日志就能定位最怕的是两眼一抹黑拿不到现场数据。5.3 几个容易被忽略但很加分的扩展点第一全局异常捕获。WPF默认遇到未处理异常会直接崩溃在App.xaml.cs挂上全局异常事件把异常堆栈写入日志并友好提示用户这个简单操作能帮你挽回大量差评。第二自动化更新。工厂客户端不可能每次去现场手动升级我建议在项目中期就部署一个简易的自动更新模块从共享目录或者FTP拉取版本清单比对后自动下载覆盖。这个模块虽然增加了工作量但后面迭代省的时间是指数级的。第三DPI感知。工厂里的显示器五花八门从14寸笔记本到55寸大屏都有。不给WPF应用设置DPI感知高分屏下整个界面会模糊。在app.manifest里加上PerMonitorV2配置同时做布局时尽量避免固定像素尺寸多用Grid和比例。第四3D场景的轻量可视化。如果车间布局展示用了Viewport3D可以用滚动条控制PerspectiveCamera的位置和角度实现类似漫游的效果。思路是滚动条绑定Camera的Position属性通过计算基于当前视角方向的位置偏移改完刷新视图。这个需求不常有一旦领导提出来能临时顶一下但不要指望用它替代专业的数字孪生方案。6. 写在最后的经验卡片做了这几个项目我最大的体会是WPF智慧工厂数据平台真正难的不是某个控件或者某个特效而是如何在长达半年的开发周期里让项目代码始终保持清晰可维护。设计模式不是用来炫技的Prism也不是装点门面的它们存在的意义是在你接手三个月前的代码时还能一眼看出这个模块是干什么的、数据从哪来、到哪里去。有几个习惯我现在已经固定下来了每个ViewModel尽量瘦身超过300行代码就考虑拆服务所有设备通信的原始依赖都藏在接口后面UI层永远不出现Modbus TCP的寄存器地址日志从开发第一天就接好不要等项目崩溃了才想起埋点。另外就是现场运维人员的反馈一定要听他们觉得顺手系统才算真正落地。最后分享一个小技巧。实时刷新页面的数据更新不要一个属性变动就通知一次界面我习惯把设备数据封装成不可变的快照对象采集服务每轮产生一个新的DeviceDataSnapshotViewModel只收到一个通知界面一次性刷新。这个改动看似简单实际对整体的流畅度提升非常明显。你的项目如果也遇到界面刷新越来越卡、CPU占用居高不下可以优先检查这一块。
返回列表