【DFX系列】Flutter 鸿蒙应用卡顿丢帧定位指南
今天这篇是卡顿丢帧专项,会从DFX事件层面进行定位讲解,包括HiAppEvent和HiLog的使用,同时需要结合DevTools,专业性会比较高。我们直接切入正题。
一、什么是丢帧
解释丢帧前,先要知道VSYNC的概念,VSYNC英文解释是Vertical Synchronization,中文叫作“垂直同步”,我们也可以把它理解为“帧同步”。这个用来保证 CPU、GPU 生成帧的速度和 Display 刷新的速度保持一致。
在60帧的情况下,以每 16.6ms就会发出一次 VSYNC信号触发 UI 渲染更新。大约屏幕一秒刷新60次,也就是说要求 CPU 和 GPU 每秒要有处理 60 帧的能力,一帧花费的时间需要在16.6ms内。
Flutter 鸿蒙应用的目标是每秒画 60 帧,也就是每帧 16.6 毫秒内画完;如果设备支持 120 帧时,就要求提高到每帧 8.3 毫秒。如果在绘制某一帧时超时了,画面就「卡一下」,这就是丢帧,也叫JANK。
目前Flutter 鸿蒙官方解释的丢帧分为两种场景,上报的触发条件和事件如下:
| 场景 | 触发条件 | 对应事件 |
|---|---|---|
| 滑动丢帧 | 滑动时某帧 ≥50ms | OTHER_JANK_SCROLL |
| 非滑动丢帧 | 帧渲染超过目标时间 | OTHER_JANK + OTHER_JANK_STAT |
滑动丢帧只在单帧 ≥50ms 时才上报。感觉滑动卡但搜不到事件,可能是每帧都 <50ms 但持续 >16.6ms,这时候就得需要抓 Trace 分析进行专业分析了。
对于Flutter鸿蒙应用,也可以从现象进行初判,具体原因得结合工具分析:
| 看到的现象 | 严重度 | 可能的原因 |
|---|---|---|
| 列表滚动有顿挫感 | 中 | 滑动丢帧 |
| 页面切换动画不流畅 | 中 | 非滑动丢帧 |
| 偶尔卡一下然后恢复 | 低 | 偶发丢帧 |
| 持续卡顿不恢复 | 高 | 持续丢帧 |
| 动画一卡一卡的 | 中 | 帧渲染慢 |
| 卡顿伴随黑屏、白屏 | 高 | GPU 上下文丢失 |
| 视频、相机画面卡顿 | 中 | 外接纹理消费过慢 |
二、检测机制:引擎怎么处理丢帧
每一帧画完,引擎会计算这帧画了多久,和目标时间比较:没超过,正常帧就不处理了;超过了,按场景会进行分流:正在滑动时,单帧 ≥50ms 就上报 OTHER_JANK_SCROLL,<50ms 忽略并输出一条日志;不在滑动时,缓存起来,最多 10 帧,缓存满 10 帧批量上报 OTHER_JANK 加 OTHER_JANK_STAT;同时输出 HiTrace 记录丢帧详情。
滑动丢帧的日志如下:
I Flutter: Ignore scroll jank: frameCost=30000us (<50ms)
三、定位工具:日志、事件、Trace和DevTools
(1)打日志:
hdc shell hilog | grep -E "OTHER_JANK|JANK|jank|missed"
| 日志关键字 | 含义 | 什么时候出现 |
|---|---|---|
| ReportScrollJANKEvent | 滑动丢帧上报 | 滑动时某帧 ≥50ms |
| ReportJANKEvent | 非滑动丢帧上报 | 非滑动丢帧时 |
| Ignore scroll jank: frameCost=Xus | 滑动丢帧被忽略 | 滑动丢帧但 <50ms |
| Vector stops push_back | 丢帧缓存满了,10 帧 | 连续丢帧超过 10 帧 |
| skip one frame(slow consumer) | 纹理消费过慢跳帧 | 外接纹理场景 |
(2)HiAppEvent事件,三个事件各有重点参数:
| 事件 | 含义 | 重点看哪个参数 |
|---|---|---|
| OTHER_JANK | 单次丢帧详情 | missedFrames,丢了多少帧 |
| OTHER_JANK_STAT | 聚合统计 | maxFrameTime,最严重一帧画了多久 |
| OTHER_JANK_SCROLL | 滑动丢帧 | totalMissedFrames / totalFrames,丢帧率 |
(3)HiTrace,最详细:
hdc shell hitrace --trace_clock boottime -t 30 flutter -o /data/local/tmp/trace.ftrace
hdc file recv /data/local/tmp/trace.ftrace ./trace.ftrace
# 30 秒内复现卡顿,Chrome 打开 chrome://tracing 加载
(4)DevTools,也是最直观的:flutter run --ohos 启动,浏览器打开 DevTools 的 Performance 标签,点录制,复现卡顿,停止。红色的帧就是丢帧,点开能看到具体哪一步慢。

四、丢帧分析:先定严重程度,再找原因
三个指标给丢帧定级:
| 指标 | 正常 | 轻微卡顿 | 严重卡顿 | 用户能感觉到 |
|---|---|---|---|---|
| 单帧耗时 | <16ms | 16-50ms | >50ms | >50ms |
| 丢帧数 | 0 | 1-3 帧 | >3 帧 | >5 帧 |
| 丢帧率 | <5% | 5-10% | >10% | >15% |
4.1 六类原因与修复
原因 1:Dart 代码执行太慢。build() 方法里做了太多事,一帧画不完。确认方法:HiTrace 里搜 flutter::Frame,看帧的 UI 线程部分耗时长不长。四种常见情况和修法:
| 情况 | 怎么修 | 代码示例 |
|---|---|---|
| Widget 树太复杂 | 拆分 Widget,多用 const | const Text(‘hello’) |
| 每帧都在 rebuild | 控制 rebuild 范围 | shouldRepaint / shouldRebuild |
| 主线程做重计算 | compute() 移到 isolate | await compute(heavyTask, data) |
| 布局层级太深 | 简化布局,SizedBox 固定尺寸 | SizedBox(height: 80, …) |
列表场景的典型对照,每帧创建新对象导致 rebuild,和 const 加 itemExtent 的优化:
// 错误:每次 build 都创建新对象
ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) {
return Container(
color: Colors.blue,
child: Text('Item $index'),
);
},
);
// 正确:itemExtent 固定高度,引擎不用测量每个 item;const Widget
ListView.builder(
itemExtent: 80.0,
itemCount: 1000,
itemBuilder: (context, index) => const _ItemTile(),
);
原因 2:GPU 渲染太慢。Dart 代码跑完了,但 GPU 画得慢。确认方法:HiTrace 里看 flutter::Frame 的 Raster 部分耗时。五种情况和修法:
| 情况 | 怎么修 |
|---|---|
| 图片太大 | cacheWidth / cacheHeight 限制解码尺寸 |
| 图层太多 | 减少 Layer 数量,RepaintBoundary 隔离 |
| 着色器编译慢 | 预热 Shader(SkSL warmup) |
| 模糊效果太重 | 减少 BackdropFilter 使用范围 |
| 过度绘制 | RepaintBoundary 隔离复杂区域 |
// 错误:加载 4000x3000 大图,解码很慢
Image.network(url)
// 正确:限制解码尺寸
Image.network(url,
cacheWidth: (screenWidth * devicePixelRatio).toInt(),
cacheHeight: (screenHeight * devicePixelRatio).toInt(),
)
// 正确:RepaintBoundary 隔离复杂区域,避免重复绘制
RepaintBoundary(child: ComplexWidget())
原因 3:资源加载卡住了 build。在 build() 里同步加载资源,阻塞界面构建:
// 错误:build 里同步加载
Widget build(BuildContext context) {
final data = loadAssetSync();
// 阻塞 build
return Text(data);
}
// 正确:initState 里异步加载
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
final data = await rootBundle.loadString('assets/data.json');
setState(() => _data = data);
}
原因 4:内存压力导致 GC 频繁。临时对象创建太多,垃圾回收器频繁启动,每次 GC 都暂停 UI 线程。确认方法:搜 heap memory 检查内存使用量。修复:减少 build() 里的临时对象,多用 const 构造函数,限制 ImageCache 大小,复用对象。
原因 5:GPU 上下文丢失。退后台时 GPU 资源被回收,回前台重建期间帧渲染中断。确认方法:搜 GpuReclaim 日志
原因 6:外接纹理消费过慢。视频、相机生产画面太快,Flutter 消费不过来,被迫跳帧。确认方法:搜 skip one frame(slow consumer)
提前预告下:以上6点分析的详细步骤会通过内存和GPU相关专项进行讲解
4.2 滑动卡顿专项三步
第 1 步,算丢帧率。从 OTHER_JANK_SCROLL 事件取 totalMissedFrames 和 totalFrames,公式如下:
丢帧率 = totalMissedFrames / totalFrames
| 丢帧率 | 严重程度 | 用户感受 |
|---|---|---|
| <5% | 正常 | 无感知 |
| 5-10% | 轻微 | 偶有顿挫 |
| 10-30% | 明显 | 明显卡顿 |
| >30% | 严重 | 严重影响体验 |
第 2 步,看最严重的一帧。maxFrameTime 说明最坏情况:
| maxFrameTime | 严重程度 |
|---|---|
| <16ms | 正常 |
| 16-50ms | 轻微 |
| 50-100ms | 明显卡顿 |
| >100ms | 严重卡顿 |
第 3 步,抓 Trace:搜 flutter::Frame 找到耗时长的帧,展开帧事件看是 UI 线程慢还是 Raster 线程慢,搜 Flutter Hitch Time 看丢帧详情。
五、HiAppEvent 事件参数
OTHER_JANK,单次丢帧:
| 参数 | 说明 | 怎么用 |
|---|---|---|
| missedFrames | 丢了多少帧 | 1-3 轻微,>3 严重 |
| startTime / endTime | 丢帧的时间范围 | 定位发生时刻 |
OTHER_JANK_STAT,聚合统计:
| 参数 | 说明 | 怎么用 |
|---|---|---|
| totalMissedFrames | 总共丢了多少帧 | 丢帧总量 |
| maxFrameTime | 最严重一帧画了多久,毫秒 | >50ms 就是严重卡顿 |
| maxMissedFrameRate | 对应的帧率 | 60/120 正常,低值说明帧率没达标 |
OTHER_JANK_SCROLL,滑动丢帧:
| 参数 | 说明 | 怎么用 |
|---|---|---|
| maxFrameTime | 最严重一帧画了多久 | >50ms 用户能感知 |
| totalMissedFrames | 滑动期间丢了多少帧 | 配合 totalFrames 算丢帧率 |
| totalFrames | 滑动期间总帧数 | 算丢帧率 |
| recentScrollCount | 上次上报后的滑动次数 | 等于 1 说明每次滑动都丢帧 |
| frameId | 最严重丢帧的帧号 | 在 Trace 中定位具体帧 |
六、排查清单
第一步,确认丢帧: 抓全量日志;搜 OTHER_JANK_SCROLL 定滑动丢帧,搜 OTHER_JANK / OTHER_JANK_STAT 定非滑动丢帧;查 maxFrameTime 定严重程度;算丢帧率。
第二步,找原因: 搜 GpuReclaim 看 GPU 回收;搜 skip one frame 看纹理消费;搜 heap memory 看内存压力和 GC;检查主线程有没有复杂计算、同步 IO;检查 Widget 是否过度 rebuild。
第三步,Trace 分析: 抓 30 秒 Trace;搜 flutter::Frame 分析每帧耗时;区分 UI 线程慢还是 Raster 线程慢;搜 Flutter Lost Frames 看丢帧分布。
第四步,DevTools 分析: Performance 录制卡顿场景;查看红色帧;分析 Timeline 里的 build、layout、paint 耗时。
七、Trace 分析技巧
Trace 里的搜索关键词和正常表现比对:
| 搜索关键词 | 看什么 | 正常表现 |
|---|---|---|
| flutter::Frame | 每一帧的耗时 | <16ms |
| Flutter Lost Frames | 丢帧计数 | 值为 0 |
| Flutter Hitch Time | 丢帧详情 | 不出现 |
| feedFlutterWatchdog | UI 线程心跳 | 每 3 秒一次 |
帧耗时分析:搜 flutter::Frame,看每个 Frame 事件的持续时间,超过 16ms 的就是丢帧;展开帧事件,看慢在哪部分:
| 哪部分慢 | 诊断 | 怎么优化 |
|---|---|---|
| UI 线程(build/layout/paint) | Dart 代码慢 | 优化 Widget build,减少 rebuild |
| Raster 线程 | GPU 渲染慢 | 优化图片,简化图层 |
| 两者都慢 | 综合问题 | 先优化 UI,再优化 Raster |
下一篇是针对时延问题的分析,也就是跟手性问题,持续期待下~
小伙伴们记得点赞+关注
关注 CPF-Flutter 社区
“AI再牛,技术不能丢“
更多推荐



所有评论(0)