​今天这篇是卡顿丢帧专项,会从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 鸿蒙官方解释的丢帧分为两种场景,上报的触发条件和事件如下:

场景触发条件对应事件
滑动丢帧滑动时某帧 ≥50msOTHER_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 标签,点录制,复现卡顿,停止。红色的帧就是丢帧,点开能看到具体哪一步慢。
在这里插入图片描述

四、丢帧分析:先定严重程度,再找原因

三个指标给丢帧定级:

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

4.1 六类原因与修复

原因 1:Dart 代码执行太慢。build() 方法里做了太多事,一帧画不完。确认方法:HiTrace 里搜 flutter::Frame,看帧的 UI 线程部分耗时长不长。四种常见情况和修法:

情况怎么修代码示例
Widget 树太复杂拆分 Widget,多用 constconst Text(‘hello’)
每帧都在 rebuild控制 rebuild 范围shouldRepaint / shouldRebuild
主线程做重计算compute() 移到 isolateawait 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丢帧详情不出现
feedFlutterWatchdogUI 线程心跳每 3 秒一次

帧耗时分析:搜 flutter::Frame,看每个 Frame 事件的持续时间,超过 16ms 的就是丢帧;展开帧事件,看慢在哪部分:

哪部分慢诊断怎么优化
UI 线程(build/layout/paint)Dart 代码慢优化 Widget build,减少 rebuild
Raster 线程GPU 渲染慢优化图片,简化图层
两者都慢综合问题先优化 UI,再优化 Raster

下一篇是针对时延问题的分析,也就是跟手性问题,持续期待下~

小伙伴们记得点赞+关注

关注 CPF-Flutter 社区

“AI再牛,技术不能丢“

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐