Day 1 - 性能认知
建议用时:210-240 分钟
你将学会什么
- 知道 UI 卡顿通常是 UI 线程被占住。
- 知道为什么不能在按钮命令里做耗时同步操作。
- 会用
Stopwatch记录耗时。 - 会用
async命令模拟加载数据。 - 会用
IsLoading显示加载状态。 - 会先测量,再谈优化。
本页不追求高级性能优化。先建立正确认知:不要凭感觉优化,先复现、测量、定位,再修改。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“复盘与巩固”,今天继续学习“性能认知”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 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 后,当 IsLoading 为 true,按钮不可执行。
6. NotifyCanExecuteChangedFor 的作用
CanLoadProducts() 依赖 IsLoading。
所以写:
[NotifyCanExecuteChangedFor(nameof(LoadProductsCommand))]
private bool isLoading;IsLoading 变化时,命令会重新判断按钮能不能点。
7. 性能优化的正确顺序
不要一开始就猜。
正确顺序:
- 复现慢的操作。
- 用
Stopwatch记录耗时。 - 确认慢的是加载、计算、渲染还是保存。
- 只改一个点。
- 再次测量。
如果没有第 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。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。完整代码就是第一部分两个文件,下面给出检查答案。
作业要求
做一个商品加载窗口:
- 点击按钮加载商品。
- 加载时显示进度条。
- 加载时按钮不能重复点击。
- 加载完成后显示商品列表。
- 用
Stopwatch显示耗时。
必须验证的操作
| 操作 | 应该看到 |
|---|---|
| 启动窗口 | 显示 点击按钮加载商品。 |
| 点击加载商品 | 进度条显示,按钮不可重复执行 |
| 等待完成 | 列表显示 3 个商品 |
| 完成后消息 | 显示加载数量和耗时 |
如果界面卡住,按这个顺序检查
- 检查是否用了
Thread.Sleep。 - 检查命令方法是否是
async Task。 - 检查等待是否写成
await Task.Delay(...)。 - 检查
IsLoading是否在开始和结束都设置。 - 检查
isLoading是否通知了LoadProductsCommand。
今天真正要掌握的闭环
点击按钮
-> 设置 IsLoading=true
-> 显示进度条
-> Stopwatch 开始测量
-> await 等待耗时任务
-> 更新列表
-> Stopwatch 停止
-> 显示耗时
-> 设置 IsLoading=false性能认知的第一步是避免阻塞 UI,第二步才是优化速度。