返回 Flutter OH平台 DFX 问题定位导航

本文档侧重从 DFX 事件层面(HiAppEvent / HiLog)定位丢帧问题。
如需 DevTools Performance 分析、FrameTiming 打点、trace 链时延量测等深度方法,请参考 Flutter OH 滑动卡顿丢帧与时延问题分析指南


1. 什么是丢帧?

1.1 通俗解释

Flutter 的目标是每秒画 60 张画(60 帧),也就是每张画要在 16.6 毫秒内画完。如果某一张画画得太久,超过 16.6 毫秒,画面就会出现"卡顿"——这就是"丢帧"(JANK)。

打个比方:就像翻页动画书,每秒翻 60 页才能看起来流畅。如果某一页你翻了 50 毫秒才翻完,动画就会"卡"一下。

如果设备支持 120 帧(高刷新率),那每帧要在 8.3 毫秒内完成,要求更高。

1.2 两种丢帧场景

场景触发条件对应事件通俗解释
滑动丢帧滑动时某帧 ≥50msOTHER_JANK_SCROLL列表滚动时卡顿
非滑动丢帧帧渲染超过目标时间OTHER_JANK + OTHER_JANK_STAT普通场景卡顿(动画、页面切换等)

新手提示:滑动丢帧只有在单帧 ≥50ms 时才上报。如果你感觉滑动卡但没搜到事件,可能是每帧都 <50ms 但持续 >16ms,需要抓 Trace 分析。

1.3 具体现象

你看到的现象严重程度可能的原因
列表滚动时有顿挫感滑动丢帧
页面切换动画不流畅非滑动丢帧
偶尔卡一下然后恢复偶发丢帧
持续卡顿不恢复持续丢帧
动画播放一卡一卡的帧渲染慢
卡顿伴随黑屏/白屏GPU 上下文丢失
视频/相机画面卡顿外接纹理消费过慢

1.4 快速定位流程

界面卡顿/不流畅
│
├─ 在日志里搜索 "OTHER_JANK_SCROLL"
│  → 滑动丢帧(滑动时某帧 ≥50ms)
│
├─ 在日志里搜索 "OTHER_JANK""OTHER_JANK_STAT"
│  → 非滑动丢帧
│
├─ 在日志里搜索 "GpuReclaim"
│  → 确认是否伴随 GPU 回收
│
├─ 在日志里搜索 "skip one frame"
│  → 确认是否纹理消费过慢
│
└─ 抓取 HiTrace 做详细分析
   → 搜索 "Flutter Lost Frames""Flutter Hitch Time"

2. 丢帧检测机制(引擎怎么知道丢帧了?)

2.1 检测流程

每一帧画完后
│
├─ 算一下:这帧画了多久?(frame_duration = 结束时间 - 开始时间)
│
├─ 超过目标时间了吗?(>16.6ms 或 >8.3ms)
│  ├─ 没超过 → 正常帧,不管它
│  └─ 超过了 → 丢帧了!记录下来
│
├─ 现在在滑动吗?
│  ├─ 在滑动 → 如果这帧 ≥50ms 就上报 OTHER_JANK_SCROLL
│  │           如果 <50ms 就忽略(输出 "Ignore scroll jank" 日志)
│  └─ 没滑动 → 缓存起来(最多缓存 10 帧)
│     ├─ 缓存满 10 帧 → 批量上报 OTHER_JANK + OTHER_JANK_STAT
│     └─ 没满 → 继续缓存
│
└─ 同时输出 HiTrace(记录丢帧详情)

2.2 滑动丢帧的阈值

滑动场景下,只有单帧耗时 ≥50ms 才会上报。如果 <50ms 会被忽略:

# 日志里会显示被忽略的丢帧
I Flutter: Ignore scroll jank: frameCost=30000us (<50ms)

30000 微秒 = 30 毫秒,小于 50ms 所以被忽略了。但 30ms 已经超过 16.6ms 的帧预算,用户可能感觉到卡。


3. 怎么定位丢帧问题?

3.1 通过日志定位

搜索命令

hdc shell hilog | grep -E "OTHER_JANK|JANK|jank|missed"

关键日志

日志关键字含义什么时候出现
ReportScrollJANKEvent滑动丢帧上报滑动时某帧 ≥50ms
ReportJANKEvent非滑动丢帧上报非滑动丢帧时
Ignore scroll jank: frameCost=Xus (<50ms)滑动丢帧被忽略滑动丢帧但 <50ms
Vector stops push_back丢帧缓存满了(10 帧)连续丢帧超过 10 帧
skip one frame(slow consumer)纹理消费过慢跳帧外接纹理场景

3.2 通过 HiAppEvent 定位

事件含义重点看哪个参数
OTHER_JANK单次丢帧详情missedFrames(丢了多少帧)
OTHER_JANK_STAT聚合统计maxFrameTime(最严重的一帧画了多久)
OTHER_JANK_SCROLL滑动丢帧totalMissedFrames / totalFrames(丢帧率)

3.3 通过 HiTrace 定位(最详细)

# 抓取 30 秒 Trace(在这 30 秒内复现卡顿)
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

# 用 Chrome 打开 chrome://tracing → Load 文件

在 Trace 中搜索这些关键词

搜索关键词看什么
flutter::Frame每一帧画了多久(超过 16ms 的就是丢帧)
Flutter Lost Frames丢帧计数(值=丢了几帧)
Flutter Hitch Time丢帧详情(帧号、耗时、原因)
feedFlutterWatchdogUI 线程心跳(检查是否卡死)

3.4 用 DevTools Performance 分析(最直观)

flutter run --ohos
# 在浏览器打开 DevTools → Performance 标签
# 点录制 → 复现卡顿 → 停止
# 红色的帧就是丢帧,点开可以看到具体哪一步慢

4. 丢帧分析

深度分析:本节列出常见原因与快速修复。如需用 DevTools Performance Overlay / Track Widget Builds / CPU Profiler 等工具逐帧定位"是哪个 Widget、哪个阶段慢",请参考 Flutter OH 滑动卡顿丢帧与时延问题分析指南 §3「卡顿定位三板斧」。

4.1 判断丢帧严重程度

指标正常轻微卡顿严重卡顿用户能感觉到
单帧耗时<16ms16-50ms>50ms>50ms
丢帧数01-3 帧>3 帧>5 帧
丢帧率<5%5-10%>10%>15%

4.2 常见丢帧原因和修复方法

原因 1:Dart 代码执行太慢

通俗解释:你的 build() 方法里做了太多事,导致一帧画不完。

怎么确认:在 HiTrace 中搜索 flutter::Frame,看帧的 UI 线程部分耗时长不长。

常见情况和修复

情况怎么修代码示例
Widget 树太复杂拆分 Widget,多用 constconst Text('hello') 而不是 Text('hello')
每帧都在 rebuild控制 rebuild 范围shouldRepaint / shouldRebuild
主线程做重计算compute() 移到 isolatefinal result = await compute(heavyTask, data);
布局层级太深简化布局,用 SizedBox 固定尺寸SizedBox(height: 80, child: ...)

代码示例

// ❌ 错误:每帧都创建新对象,导致 rebuild
ListView.builder(
  itemCount: 1000,
  itemBuilder: (context, index) {
    return Container(
      color: Colors.blue,  // 每次 build 都创建新对象
      child: Text('Item $index'),
    );
  },
);

// ✅ 正确:用 const 和 itemExtent 优化
ListView.builder(
  itemExtent: 80.0,  // 固定高度,引擎不用测量每个 item
  itemCount: 1000,
  itemBuilder: (context, index) => const _ItemTile(),  // const Widget
);

原因 2:GPU 渲染太慢

通俗解释:Dart 代码跑得很快(build 完了),但 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);
}

// ✅ 正确:异步加载
@override
void initState() {
  super.initState();
  _loadData();  // 异步加载
}

Future<void> _loadData() async {
  final data = await rootBundle.loadString('assets/data.json');
  setState(() => _data = data);
}

原因 4:内存压力导致 GC(垃圾回收)频繁

通俗解释:你创建太多临时对象,Dart 的垃圾回收器频繁启动,每次 GC 都会暂停 UI 线程。

怎么确认:搜索 heap memory 检查内存使用量(详见 Flutter OH 内存与 GPU 问题定位指南 §2)。

修复方法

  • 减少 build() 中创建的临时对象
  • 多用 const 构造函数
  • 限制 ImageCache 大小
  • 复用对象而不是反复创建

原因 5:GPU 上下文丢失

通俗解释:应用退后台时 GPU 资源被回收了,回前台后重建期间帧渲染中断。

怎么确认:搜索 GpuReclaim 日志(详见 Flutter OH 内存与 GPU 问题定位指南 §3)。

原因 6:外接纹理消费过慢

通俗解释:视频/相机生产画面太快,Flutter 消费不过来,被迫跳帧。

怎么确认:搜索 skip one frame(slow consumer)(详见 Flutter OH 外接纹理问题定位指南 §4)。

4.3 滑动卡顿专项排查

第 1 步:算丢帧率

OTHER_JANK_SCROLL 事件中取 totalMissedFramestotalFrames

丢帧率 = totalMissedFrames / totalFrames
丢帧率严重程度用户感受
<5%正常无感知
5-10%轻微偶有顿挫
10-30%明显明显卡顿
>30%严重严重影响体验

第 2 步:看最严重的一帧

maxFrameTime 告诉你最严重的一帧画了多久:

maxFrameTime严重程度
<16ms正常
16-50ms轻微
50-100ms明显卡顿
>100ms严重卡顿

第 3 步:抓 Trace 分析

hdc shell hitrace --trace_clock boottime -t 30 flutter -o /data/local/tmp/trace.ftrace

在 Chrome 中打开后:

  1. 搜索 flutter::Frame 找到耗时长的帧
  2. 展开帧事件,看是 UI 线程慢还是 Raster 线程慢
  3. 搜索 Flutter Hitch Time 看丢帧详情

5. HiAppEvent 事件参数

5.1 OTHER_JANK(单次丢帧)

参数说明怎么用
missedFrames丢了多少帧1-3 轻微,>3 严重
startTime / endTime丢帧的时间范围定位发生时刻

5.2 OTHER_JANK_STAT(聚合统计)

参数说明怎么用
totalMissedFrames总共丢了多少帧丢帧总量
maxFrameTime最严重的一帧画了多久(毫秒)>50ms 就是严重卡顿
maxMissedFrameRate对应的帧率60/120 是正常,低值说明帧率没达标

5.3 OTHER_JANK_SCROLL(滑动丢帧)

参数说明怎么用
maxFrameTime最严重的一帧画了多久>50ms 用户能感知
totalMissedFrames滑动期间丢了多少帧配合 totalFrames 算丢帧率
totalFrames滑动期间总帧数算丢帧率
recentScrollCount上次上报后的滑动次数1 = 每次滑动都丢帧
frameId最严重丢帧的帧号在 Trace 中定位具体帧

6. 排查清单

第一步:确认丢帧

  • 抓取全量日志:hdc shell hilog > flutter_log.txt
  • 搜索 OTHER_JANK_SCROLL → 滑动丢帧
  • 搜索 OTHER_JANK / OTHER_JANK_STAT → 非滑动丢帧
  • 检查 maxFrameTime 确定严重程度
  • 算丢帧率(totalMissedFrames / totalFrames

第二步:找原因

  • 搜索 GpuReclaim → 是否 GPU 回收导致
  • 搜索 skip one frame → 是否纹理消费过慢
  • 搜索 heap memory → 是否内存压力导致 GC
  • 检查 Dart 代码是否有复杂计算 / 同步 IO 在主线程
  • 检查 Widget 是否过度 rebuild

第三步:Trace 分析

  • 抓取 Trace:hdc shell hitrace --trace_clock boottime -t 30 flutter
  • 搜索 flutter::Frame 分析每帧耗时
  • 区分 UI 线程慢还是 Raster 线程慢
  • 搜索 Flutter Lost Frames 看丢帧分布

第四步:DevTools 分析

  • 用 DevTools Performance 录制卡顿场景
  • 查看红色帧(丢帧)
  • 分析 Timeline 中的 build/layout/paint 耗时

7. HiTrace 分析技巧

7.1 打开 Trace 文件

  1. .ftrace 文件拉取到电脑
  2. 打开 Chrome 浏览器,访问 chrome://tracing
  3. 点击 Load 加载文件
  4. 快捷键:w 放大、s 缩小、a 左移、d 右移

7.2 搜索什么关键词?

搜索关键词看什么正常表现
flutter::Frame每一帧的耗时<16ms
Flutter Lost Frames丢帧计数值为 0
Flutter Hitch Time丢帧详情不出现
feedFlutterWatchdogUI 心跳每 3 秒一次

7.3 分析帧耗时

  1. 搜索 flutter::Frame
  2. 看每个 Frame 事件的持续时间
  3. 超过 16ms 的就是丢帧
  4. 展开帧事件,看是哪部分慢:
哪部分慢诊断怎么优化
UI 线程(build/layout/paint)Dart 代码慢优化 Widget build、减少 rebuild
Raster 线程GPU 渲染慢优化图片、简化图层
两者都慢综合问题先优化 UI,再优化 Raster

7.4 API 版本影响

API 版本Trace 能力
< 19只有丢帧计数,没有详情
≥ 19同时有计数和详情(推荐)
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐