Skip to Content
Week 09Day 1 - 性能认知

Day 1 - 性能认知

建议用时:210-240 分钟

你将学会什么

  • 知道 UI 卡顿通常是 UI 线程被占住。
  • 知道为什么不能在按钮命令里做耗时同步操作。
  • 会用 Stopwatch 记录耗时。
  • 会用 async 命令模拟加载数据。
  • 会用 IsLoading 显示加载状态。
  • 会先测量,再谈优化。

本页不追求高级性能优化。先建立正确认知:不要凭感觉优化,先复现、测量、定位,再修改。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“复盘与巩固”,今天继续学习“性能认知”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 3 项,就可以继续:

  • 能用自己的话说明“性能认知”解决什么问题。
  • 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
  • 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。

第一部分:先学原理和最小知识

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。性能认知先抓住三件事:卡在哪里、怎么测、怎么避免阻塞。

1. UI 线程是什么

桌面应用里,界面绘制和用户操作通常由 UI 线程处理。

它要负责:

  • 绘制窗口。
  • 响应鼠标点击。
  • 响应键盘输入。
  • 刷新控件状态。

如果你在 UI 线程里做耗时操作,界面就没时间刷新,看起来就是卡住。

2. 什么叫阻塞 UI

阻塞 UI 就是让 UI 线程长时间不能处理界面消息。

典型错误:

Thread.Sleep(3000);

如果这行代码在按钮命令里执行,3 秒内窗口可能无法响应。

本页使用:

await Task.Delay(800);

它模拟等待,但不会把 UI 线程一直占住。

3. Stopwatch 是什么

Stopwatch 用来测量代码耗时。

var watch = Stopwatch.StartNew(); await Task.Delay(800); watch.Stop(); Message = $"耗时 {watch.ElapsedMilliseconds}ms";

性能优化之前必须先有基准数据。没有测量,就不知道改动有没有效果。

4. IsLoading 是什么

IsLoading 是界面状态。

private bool isLoading;

它可以控制:

  • 进度条是否显示。
  • 按钮是否可执行。
  • 文案显示“正在加载”还是“加载完成”。

5. 为什么 LoadProducts 要有 CanExecute

加载中不要重复点击加载按钮。

private bool CanLoadProducts() { return !IsLoading; }

按钮绑定 LoadProductsCommand 后,当 IsLoadingtrue,按钮不可执行。

6. NotifyCanExecuteChangedFor 的作用

CanLoadProducts() 依赖 IsLoading

所以写:

[NotifyCanExecuteChangedFor(nameof(LoadProductsCommand))] private bool isLoading;

IsLoading 变化时,命令会重新判断按钮能不能点。

7. 性能优化的正确顺序

不要一开始就猜。

正确顺序:

  1. 复现慢的操作。
  2. Stopwatch 记录耗时。
  3. 确认慢的是加载、计算、渲染还是保存。
  4. 只改一个点。
  5. 再次测量。

如果没有第 2 步,优化很容易变成猜测。

8. 常见卡顿来源

来源例子第一反应
同步等待Thread.Sleep、同步 IO改成异步
一次加载太多一次显示几万条分页或虚拟化
重复计算每次刷新都重新排序缓存或减少刷新
图片太大列表加载大量图片缩略图、延迟加载
频繁搜索每输入一个字就全量过滤按钮触发或防抖

性能排查常用 API 速查

需求写法说明
计时Stopwatch.StartNew()测一段代码耗时
停止计时watch.Stop()得到最终耗时
毫秒数watch.ElapsedMilliseconds输出耗时
大量数据Enumerable.Range(...)构造测试数据
只取前几条Take(100)减少界面显示压力
延迟执行LINQ 查询ToList() 时固定结果

性能排查顺序:

先测量 -> 找到慢在哪里 -> 减少不必要刷新 -> 减少一次性显示数据 -> 再考虑后台任务

第二部分:把知识组合成完整例子

今天做一个“加载商品”窗口:

  • 点击加载按钮。
  • 显示加载中。
  • 模拟耗时 800 毫秒。
  • 加载完成后显示商品列表和耗时。
  • 保存耗时结果,作为优化前的基准。

ViewModels/MainWindowViewModel.cs

using System.Collections.ObjectModel; using System.Diagnostics; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ProductApp.ViewModels; public partial class MainWindowViewModel : ViewModelBase { public ObservableCollection<string> Products { get; } = new(); [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(LoadProductsCommand))] private bool isLoading; [ObservableProperty] private string message = "点击按钮加载商品。"; private bool CanLoadProducts() { return !IsLoading; } [RelayCommand(CanExecute = nameof(CanLoadProducts))] private async Task LoadProducts() { IsLoading = true; Message = "正在加载商品..."; var watch = Stopwatch.StartNew(); await Task.Delay(800); Products.Clear(); Products.Add("Keyboard"); Products.Add("Mouse"); Products.Add("Monitor"); watch.Stop(); Message = $"加载完成:{Products.Count} 条,耗时 {watch.ElapsedMilliseconds}ms"; IsLoading = false; } }

Views/MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:ProductApp.ViewModels" x:Class="ProductApp.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Width="620" Height="420" Title="性能认知"> <StackPanel Margin="20" Spacing="10"> <TextBlock Text="商品加载" FontSize="24" FontWeight="Bold" /> <Button Content="加载商品" Command="{Binding LoadProductsCommand}" /> <ProgressBar IsIndeterminate="True" IsVisible="{Binding IsLoading}" /> <TextBlock Text="{Binding Message}" TextWrapping="Wrap" /> <ListBox ItemsSource="{Binding Products}" /> </StackPanel> </Window>

先用一句话理解

耗时操作不要占住 UI 线程;用 async 等待耗时任务,同时用 IsLoading 告诉界面现在正在处理。

第三部分:跟着敲代码

从这里开始动手。

例子 1:只测量耗时

先理解 Stopwatch

using System.Diagnostics; var watch = Stopwatch.StartNew(); await Task.Delay(800); watch.Stop(); Console.WriteLine($"耗时 {watch.ElapsedMilliseconds}ms");

这段代码的重点不是 800 毫秒,而是“先测量”。

例子 2:把测量放进 ViewModel

使用第一部分的 MainWindowViewModel.cs。重点看:

var watch = Stopwatch.StartNew(); await Task.Delay(800); watch.Stop();

例子 3:用 IsLoading 控制界面

XAML 里:

<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsLoading}" />

ViewModel 里:

IsLoading = true; ... IsLoading = false;

这就是状态驱动界面。

小白重复敲写训练

性能先练测量,不凭感觉说快慢。

训练 1:用秒表测循环

var watch = Stopwatch.StartNew(); long sum = 0; for (int i = 0; i < 1_000_000; i++) { sum += i; } watch.Stop(); Console.WriteLine($"{sum} - {watch.ElapsedMilliseconds}ms");

改动任务:把次数改成一千万,比较时间。

训练 2:比较重复查找

var list = Enumerable.Range(1, 100000).ToList(); var set = list.ToHashSet(); Console.WriteLine(list.Contains(99999)); Console.WriteLine(set.Contains(99999));

第二遍把查找放进循环并分别计时。

训练 3:避免循环里重复计算

var items = Enumerable.Range(1, 1000).ToList(); int count = items.Count; for (int i = 0; i < count; i++) { _ = items[i]; }

第三遍写下“先测量、再定位、最后优化”的顺序。

每日小测

做完本页后,用这 5 题检查是否真的掌握。

1. 判断题

本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。

答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。

2. 填空题

本页主题是:性能认知。今天至少要掌握的 3 个点是:

1. 知道 UI 卡顿通常是 UI 线程被占住。 2. 知道为什么不能在按钮命令里做耗时同步操作。 3. 会用 `Stopwatch` 记录耗时。

答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。

3. 流程题

遇到本页相关功能时,先按什么顺序处理?

答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。

4. 找错误题

如果本页代码运行失败,第一步应该做什么?

答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。

5. 改需求题

在本页完整例子跑通后,至少改一个小需求。

可选改法:

  • 改一个字段名称。
  • 多加一个校验条件。
  • 多输出一行结果。
  • 把固定数据改成用户输入。
  • 把一次处理改成多条数据处理。

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:知道 UI 卡顿通常是 UI 线程被占住。。

上位机专项练习

实时刷新不能每次都重建整个列表。先减少无效更新,再控制刷新频率。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:只在值变化时通知

public double Temperature { get => temperature; set => SetProperty(ref temperature, value); }

运行结果或界面效果:

相同值不会重复触发界面更新

改动任务: 对 Pressure 使用同样写法。

专项例子 2:批量更新后一次刷新

foreach (Reading reading in newReadings) { cache[reading.TagName] = reading.Value; } RefreshSummary();

运行结果或界面效果:

先更新缓存,再统一刷新摘要

改动任务: 记录 RefreshSummary 被调用几次。

专项例子 3:限制界面刷新频率

if (DateTime.Now - lastUiUpdate < TimeSpan.FromMilliseconds(200)) { return; } lastUiUpdate = DateTime.Now; UpdateDashboard();

运行结果或界面效果:

采集可很快,界面最多每 200 ms 刷新一次

改动任务: 把刷新间隔改成 500 ms。

第四部分:作业完整答案

这一部分给出当天作业的完整答案。完整代码就是第一部分两个文件,下面给出检查答案。

作业要求

做一个商品加载窗口:

  1. 点击按钮加载商品。
  2. 加载时显示进度条。
  3. 加载时按钮不能重复点击。
  4. 加载完成后显示商品列表。
  5. Stopwatch 显示耗时。

必须验证的操作

操作应该看到
启动窗口显示 点击按钮加载商品。
点击加载商品进度条显示,按钮不可重复执行
等待完成列表显示 3 个商品
完成后消息显示加载数量和耗时

如果界面卡住,按这个顺序检查

  1. 检查是否用了 Thread.Sleep
  2. 检查命令方法是否是 async Task
  3. 检查等待是否写成 await Task.Delay(...)
  4. 检查 IsLoading 是否在开始和结束都设置。
  5. 检查 isLoading 是否通知了 LoadProductsCommand

今天真正要掌握的闭环

点击按钮 -> 设置 IsLoading=true -> 显示进度条 -> Stopwatch 开始测量 -> await 等待耗时任务 -> 更新列表 -> Stopwatch 停止 -> 显示耗时 -> 设置 IsLoading=false

性能认知的第一步是避免阻塞 UI,第二步才是优化速度。