欢迎光临
我们一直在努力

【DFX系列】Flutter 鸿蒙应用滑动卡顿与时延问题分析

本篇是DFX系列的第六篇,会详细讲解Flutter鸿蒙应用滑动卡顿问题与时延问题,我们直接进入主题。

一、渲染模型:一帧的时间分布

Flutter鸿蒙应用的每一帧会经过四个阶段才能完成上屏,前三个都 UI 线程,最后一个在 Raster 线程。帧预算(60Hz 对应 16.6ms、120Hz 对应 8.3ms)在上一篇讲解过,这里我们直接看下各阶段如何慢了到底意味着什么:

阶段对应线程做什么什么原因
Build UI 执行 Dart 代码,更新 Widget 树 Dart 代码太复杂
Layout UI 确定每个组件的大小和位置 布局层级太深
Paint UI 生成绘制指令(Layer 树) 绘制指令太多
Composite Raster 把 Layer 树变成像素,经鸿蒙 RenderService 合成上屏 画面太复杂:包括大图、模糊、大量图层等

四个核心指标:

指标用什么方法查看注意事项
FPS / 丢帧率 Overlay 或 SmartPerf 平均值有时会掩盖偶发大卡顿,需要综合看 P90/P99 帧的耗时
帧耗时 DevTools Frames 图表 UI 和 Raster 需要分开查看
响应时延 trace 链 / 打点 超过 100ms 用户感觉「迟钝」
完成时延 打点(t2-t0) 响应合格不代表完成合格

经验分享:有时候别被平均 FPS 骗了,比如平均 55fps 看着不错,但只要中间有那么1-2帧开销200ms,用户一定觉得卡。这时候就得看 P90/P99帧耗时和超时帧计数了 。P90/P99 帧,简单说就是把一帧画面的渲染耗时按从小到大排序 , ‌ P90 表示 90% 的帧都在这个耗时以内,P99 表示 99% 的帧都在这个耗时以内 。

响应时延是「点了有没有立刻有反应」,主线程被阻塞;

完成时延是「操作有没有彻底完成」,数据加载慢、首屏构建太大也会慢。

二、卡顿定位

整体方法:1.Overlay 看走势(哪条线程红) 2.DevTools 定界(哪个阶段超时) 3.逐帧实锤(具体原因)。

2.1 Performance Overlay 看走势

一行代码开启,屏幕顶部出现两条柱状图:MaterialApp(

  showPerformanceOverlay: true,  // 开启性能监控浮层  home: MyApp(),);

柱状图位置代表什么变红说明
上方 Raster 线程(GPU 绘制) 绘制太重:大图、模糊、saveLayer
下方 UI 线程(Dart 代码) Dart 代码慢:build/layout/paint
两条都红 两条线程都超时 一般是重建过大引发的连锁反应

2.2 DevTools Performance 定界

必须用 profile 模式,debug 数据不准:

flutter run –profile

浏览器打开 DevTools 的 Performance 页:Flutter Frames 图表里,超预算的帧标红,Shader 编译导致的掉帧有专门标记;点选某一帧,下方分开展示 UI 与 Raster 两条时间线。

2.3 逐帧实锤

在 DevTools Performance 页勾上 Enhance Tracing 三件套:

勾选什么看什么找什么
Track Widget Builds 每个 Widget 的 build 耗时 不该Build却在Build的 Widget
Track Layouts 布局耗时 layout 阶段的重灾区
Track Paints 绘制耗时 paint 阶段的重灾区

拿到「超时帧 + 具体 Widget + 具体阶段」,Flutter 侧证据链就闭合了。可以通过CPU Profiler 采样看 Dart 函数级耗时,找 build 阶段里的问题函数;

三、常见卡顿真凶与修复

3.1 Build 阶段(UI 线程)

setState 粒度过大: 一处变化,全页触发Build。

举例:缺 const, 同样的子树每帧重复构建:

// 错误:每次父级重建,静态头也跟着重来
Widget build(_) => Column(children: [Header(), …]);
// 正确:const 构造,Flutter 直接复用实例
Widget build(_) => Column(children: [const Header(), …]);

在 build 里干重活: 同步读文件、解析 JSON、跑复杂计算,build 该是纯函数气质,重活交给 compute

3.2 列表

不定高 + 无 prototype: 每次滚动都要现算每个 item 的高度,导致卡顿。定高列表直接给 itemExtent,正确案例如下:

ListView.builder(
  itemExtent: 100,
  itemCount: data.length,
  itemBuilder: (context, i) => ItemCell(data[i]),
);

一次性构建几百个 item:

// 错误:全构建了
ListView(children: items.map((e) => ItemWidget(e)).toList());
// 正确:builder 懒加载
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) => ItemWidget(items[index]),
);

itemBuilder 里同步 IO / 重组数据: itemBuilder 在滚动中被高频调用,里面只允许轻量组装。

3.3 图片

4K 原图塞进 200px 的卡片:导致解码和显存撑爆

// 正确:按显示尺寸解码
Image.network(url, cacheWidth: 200, cacheHeight: 200);

3.4 时延类

主线程同步重活: 大 JSON 解析、批量读写、加解密全在 UI 线程,导致时延

首屏一次性全量构建: 新页面一次性 build 几百个 Widget,完成时延必炸。可以通过:分页、懒加载、骨架屏先给响应,数据渐进填充进行优化。

四、怎么度量时延

4.1 FrameTiming API:开发期自查

import 'package:flutter/scheduler.dart';
void startFrameWatch() {
  SchedulerBinding.instance.addTimingsCallback((List<FrameTiming> timings) {
    const budget = Duration(milliseconds: 16);  
// 60Hz 帧预算
    for (final t in timings) {
      if (t.buildDuration > budget || t.rasterDuration > budget) {
        
// 超时帧:读 t.buildDuration / t.rasterDuration / t.totalSpan
        
// 落日志或上报,攒多了就是丢帧率
      }
    }
  });
}

配合打点量化时延:手势回调记 t0,首帧 addPostFrameCallback 记 t1,数据就绪且渲染稳定记 t2:

时延计算通俗解释
响应时延 t1 – t0 点了到有反应花了多久
完成时延 t2 – t0 点了到彻底完成花了多久

4.2 系统 trace 链

用 DevEco Profiler 抓 trace,两个工具分工:

工具适合场景
DevEco Profiler trace 分析、时延实锤,开发期深度分析
HiSmartPerf(Host 端 / 设备端 SP_daemon) 按包名采集 FPS、丢帧、CPU,脱离 IDE 的场景化巡检,适合走查和回归留档

时延口径:对外出数以 trace 链为准(mmi_service 按下 → RSHardwareThread 首帧渲染结束);FrameTiming 打点是开发期自查;

4.3 自动化回归:防劣化

滑动场景的帧耗时塞进 CI 做基线巡检,回归劣化直接报警:

testWidgets('list scroll perf', (tester) async {
  app.main();
  await tester.pumpAndSettle();
  await tester.binding.traceAction(() async {
    await tester.fling(find.byType(Scrollable), const Offset(0, -600), 8000);
    await tester.pumpAndSettle();
  }, reportKey: 'scrolling_timeline');
});

五、案例集

案例1:整页 setState 改局部刷新,治掉帧

问题:一个计数器变化,Header 和长列表全跟着重建:

// 错误写法
class _BadPageState extends State<BadPage> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        HeavyHeader(),                  
// 无辜躺枪,每次跟着重建
        Expanded(child: HeavyList()),  
// 无辜躺枪 ×2
        Text('$_count'),
        ElevatedButton(
          onPressed: () => setState(() => _count++),
          child: const Text('+1'),
        ),
      ],
    );
  }
}

修复:可变状态下沉到最小子树,静态部分全部 const 化,只有 Counter 自己会 rebuild:

// 正确写法
class GoodPage extends StatelessWidget {
  const GoodPage({super.key});
  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        HeavyHeader(),                
// const 构造,不再重复重建
        Expanded(child: HeavyList()),
        Counter(),                    
// 只有它自己会 rebuild
      ],
    );
  }
}
class _CounterState extends State<Counter> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('$_count'),
        ElevatedButton(
          onPressed: () => setState(() => _count++),
          child: const Text('+1'),
        ),
      ],
    );
  }
}

复测对比:Track Widget Builds 里每次点击的重建数量,从「整页」降到「1 个子树」。

案例2:首屏完成时延爆炸,compute 卸载

问题:大 JSON 在主线程解析加映射,点击后首帧迟到一个身位:

// 错误写法:全在 UI 线程
Future<List<Item>> loadItems() async {
  final raw = await rootBundle.loadString('assets/items.json');
  final list = (jsonDecode(raw) as List).cast<Map<String, dynamic>>();
  return list.map(Item.fromJson).toList();
}

修复:解析下沉到后台 isolate,主线程只负责渲染:

// 正确写法:compute 要求顶层或静态函数
import 'package:flutter/foundation.dart';
Future<List<Item>> loadItems() async {
  final raw = await rootBundle.loadString('assets/items.json');
  return compute(_parseItems, raw);
}
List<Item> _parseItems(String raw) {
  final list = (jsonDecode(raw) as List).cast<Map<String, dynamic>>();
  return list.map(Item.fromJson).toList();
}

修完后同场景打点:完成时延 t2-t0 显著回落,响应时延也不再被解析拖住。

六、FAQ

Q1:debug 模式卡得要死,是问题吗?

不一定是。debug 有断言和 JIT 开销,一切结论以 profile 包为准。

Q2:Overlay 两条杠怎么分工?

上方是 Raster(GPU)线程,,下方是 UI 线程,图上有文字标注;红柱等于超帧预算。

Q3:DevTools 标了「Shader Compilation Jank」,怎么治?

Skia 首次编译 shader 导致。所用 OHOS 引擎版本支持 Impeller 就优先开;Skia 后端用 –cache-sksl 预热。

Q4:响应、完成时延怎么测才算标准口径?

滑动响应时延以 trace 链为准:mmi_service 按下 → 滑动首帧在 RSHardwareThread 渲染结束,自动化测试看 dpu_gfx_primary。Flutter 侧打点(t0 触摸 → t1 首帧 → t2 稳定)是开发期自查,两边对齐后按华为应用体验质量标准的术语出报告。

Q5:平均 FPS 看着挺高,用户还说卡?

平均值会掩盖偶发大卡顿。看 P90/P99 帧耗时和超时帧计数,别被均值骗了。


DFX系列越写内容涉及的越多,后面可能会调整下计划,优先讲解几个关键工具,避免大家不理解,下一期大家期待下~

小伙伴们记得点赞+关注

关注 CPF-Flutter 社区

“AI再牛,技术不能丢

赞(0)
未经允许不得转载:171主机测评 » 【DFX系列】Flutter 鸿蒙应用滑动卡顿与时延问题分析
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址