1、卡顿现象初探:什么是页面切换卡顿?卡顿的直观表现与用户感知

做嵌入式 GUI 开发的朋友,十有八九都遇到过这个问题:

手指在屏幕上划了一下,页面没反应。再划一下,还是没反应。过了半秒到一秒,画面突然“跳”到下一页。嗯,这就是典型的页面切换卡顿。

我刚开始用 TouchGFX 做项目时,第一次遇到这情况,还以为是硬件坏了。后来才发现,这其实是软件层面的问题,而且非常普遍。

1.1 卡顿的本质:帧率掉队

说白了,页面切换卡顿就是屏幕的刷新率没跟上你的操作节奏。

正常流畅的界面,每秒要刷新 60 帧(也就是 60 FPS)。每一帧的渲染时间大约 16.7 毫秒。如果某一帧的渲染时间超过了这个窗口,下一帧就会延迟,人眼就能感知到“卡”了一下。

核心指标:

  • 流畅: 帧率稳定在 55~60 FPS,每帧耗时 ≤ 17ms
  • 轻微卡顿: 帧率掉到 30~40 FPS,每帧耗时 25~33ms
  • 严重卡顿: 帧率低于 20 FPS,每帧耗时 > 50ms,肉眼可见的“跳帧”

为什么会这样?因为页面切换时,系统要做的事情突然变多了。要加载新界面的控件、要解码图片、要重新计算布局、还要释放旧界面的内存。这些事情挤在几毫秒内完成,CPU 和 GPU 很容易就扛不住了。

1.2 卡顿的直观表现:你看到的 vs 你感觉到的

我习惯把卡顿的表现分成两类:视觉上的和操作上的。这两类往往同时出现。

表现类型 具体现象 用户感知
视觉卡顿 画面撕裂、掉帧、动画不连贯、页面“闪一下” “这界面好廉价”、“反应好慢”
操作延迟 点击后无反馈、滑动后页面滞后、触摸响应“慢半拍” “是不是死机了?”、“我再点一下试试”
资源过载 切换时屏幕短暂黑屏、白屏、或显示上一页的残留内容 “这产品不成熟”、“体验很差”

你想想看,用户不会管你底层用了什么算法、内存有多大。他只知道:点下去没反应,就是卡。这个印象一旦形成,很难扭转。

我的经验: 有一次做智能家居面板,客户反馈“界面不跟手”。我拿示波器量了一下触摸响应到画面更新的时间,发现从触摸中断触发到第一帧渲染完成,整整花了 120ms。用户能明显感觉到“滞后”。后来优化到 30ms 以内,客户才满意。所以,卡顿不光是帧率问题,响应延迟同样致命。

1.3 卡顿的“罪魁祸首”:谁在拖慢你的页面切换?

我在项目中排查过很多次卡顿问题,总结下来,元凶不外乎这几个:

  1. 图片解码太慢 —— 尤其是大尺寸的 PNG 或带透明通道的图片。TouchGFX 在切换页面时,如果新页面包含大量未缓存的图片,解码时间会直接吃掉好几帧。
  2. 动态内存分配(malloc) —— 页面切换时频繁创建和销毁控件,导致堆内存碎片化,分配耗时越来越长。
  3. 渲染管线阻塞 —— 比如在帧回调里做了耗时操作(读写文件、打印日志),导致 GPU 等 CPU,或者 CPU 等 GPU。
  4. 无效的控件重绘 —— 有些控件明明没变化,却被标记为“脏区域”,导致每一帧都在重复绘制。
  5. 帧缓冲区切换策略不当 —— 双缓冲或三缓冲的配置不合理,造成屏幕撕裂或等待垂直同步(VSync)超时。

注意: 很多人一上来就怀疑硬件性能不够,其实大部分卡顿都是软件层面的“不合理操作”造成的。我曾经在一个项目里,发现页面切换时调用了 HAL::getInstance()->vSync() 的阻塞等待,导致主线程被挂起 10ms。去掉之后,帧率直接从 25 FPS 升到了 55 FPS。所以,先查软件,再换硬件。

1.4 卡顿的“用户感知曲线”:从流畅到不可用

人眼对帧率的敏感度不是线性的。我画了一张图,帮你直观理解:

帧率与用户感知曲线 用户满意度 帧率 (FPS) 15 FPS 严重卡顿 30 FPS 轻微卡顿 45 FPS 基本流畅 60 FPS 极致流畅 关键阈值: • 低于 20 FPS → 用户明显感知卡顿 • 30 FPS 以上 → 基本可接受

从图上能看出来:

  • 帧率从 60 掉到 45,用户几乎感觉不到差异。
  • 从 45 掉到 30,开始觉得“有点慢”。
  • 一旦低于 20,用户就会直接抱怨“卡死了”。

所以,我们的优化目标不是非得跑到 60 FPS。能把帧率稳定在 30 以上,用户感知就已经是“流畅”了。当然,能跑满 60 最好。

1.5 如何“看见”卡顿?—— 测量是优化的第一步

我个人的习惯是:先量化,再优化。没有数据,你根本不知道卡顿到底卡在哪里。

在 TouchGFX 里,最简单的测量方法就是打开帧率显示:

// 在 main.cpp 或你的 Application 初始化中启用
HAL::getInstance()->setFrameRateMeasure(true);

// 然后在屏幕上显示帧率
// 通常可以在 DebugPrinter 或自定义控件中读取
uint16_t currentFPS = HAL::getInstance()->getFrameRate();

这样,屏幕上就会实时显示当前的帧率。你切换页面时,盯着这个数字看:

  • 如果切换瞬间帧率掉到 10 以下,说明有严重的阻塞操作。
  • 如果帧率在 20~30 之间波动,说明渲染负载过高。
  • 如果帧率稳定在 55 以上,但用户还是觉得卡,那可能是触摸响应延迟的问题。

一个小技巧: 除了看帧率,我还会用 GPIO 示波器法。在页面切换的入口和出口各拉一个 GPIO 引脚,用示波器量一下高电平持续了多久。这样能精确知道页面切换消耗了多少毫秒,比看帧率更直观。我曾经用这个方法发现,某个页面切换竟然花了 800ms,而其中 600ms 都花在了解码一张 800x480 的背景图上。

1.6 本章小结:卡顿不可怕,可怕的是不知道它为什么卡

页面切换卡顿,说白了就是系统在切换瞬间“忙不过来”。它的表现很直观:掉帧、延迟、画面残留。它的根源也很集中:图片解码、内存分配、渲染阻塞、无效重绘。

从下一章开始,我们会逐个击破这些元凶。但在此之前,我希望你记住一句话:没有测量,就没有优化。先想办法看到卡顿,再谈怎么解决它。

本章核心要点:

  • 卡顿的本质是帧率掉队,每帧渲染时间超过 17ms 就会出问题。
  • 用户感知的“卡”不只是帧率低,还包括操作响应延迟。
  • 常见的卡顿元凶有 5 个:图片解码、内存分配、渲染阻塞、无效重绘、缓冲策略。
  • 优化目标:稳定在 30 FPS 以上,用户就基本满意了。
  • 测量工具:帧率显示、GPIO 示波器法,选一个用起来。