1. 概述

用户反馈"页面卡"时,有两类常见的时延问题:响应时延过长和完成时延过长。

响应时延一般指的是从手指触摸事件产生,到屏幕上出现第一个可见反馈帧的时间。Flutter 里这通常对应:触摸事件经系统分发到 Flutter 主线程 → 主线程处理并触发 rebuild → 生成 Layer tree → 渲染线程栅格化 → 送屏。

完成时延一般指的是从用户操作开始,到整个操作彻底完成(页面渲染稳定、数据全部展示)的时间。它包含响应时延,还包括数据加载、图片解码、复杂布局等后续工作。

2. Flutter 渲染管线介绍

Flutter 渲染一帧,大致经过四个阶段:

阶段 做什么 大致耗在哪条线程
Build 根据状态变化构建/更新 Widget 树、Element 树 UI 线程(主线程)
Layout RenderObject 树计算每个节点的大小和位置 UI 线程
Paint 生成绘制指令,构建 Layer tree UI 线程
Composite 将 Layer tree 栅格化成像素,输出到屏幕 Raster 线程(渲染线程/GPU 线程)

其中UI 线程:运行 Dart VM,执行你的 Dart 代码和 Flutter 框架代码,产出 Layer tree;Raster 线程:接收 Layer tree,调用 Skia 或 Impeller 进行栅格化与合成,最终送屏。

性能分析时,两条线程的时间都要看:UI 线程耗时过长,说明 Dart 层有性能问题;Raster 线程耗时过长,说明绘制太重(阴影、模糊、大图、saveLayer 等)。

后续主要分析都围绕 UI 线程和 Raster 线程展开,需要先学会通过trace确认它们的 tid。通过查看 trace 的线程清单,重点关注以下角色分类:

角色 典型线程名 说明
flutter_ui 1.uiroot.1.uiroot.N.ui Flutter UI 线程,运行 Dart 代码
platform_main 应用包名(如 .my_app 应用主线程
flutter_raster 1.rasterroot.1.raster Flutter Raster 线程,执行光栅化

Flutter 3.35+ 线程合并:从 Flutter 3.35 版本起,OHOS 平台上 UI 线程与应用主线程可能合并为同一条线程,线程名为应用包名(如 .complex_layout),被归类为 platform_main 而非 flutter_ui。此时不能仅靠线程名判断 UI 线程,而应通过搜索 flutter::Animator::BeginFrame trace 来定位——该 slice 所在的 tid 即为 UI 线程。

3. 基于 OHOS平台Trace 的点击响应/完成时延定位

3.1 响应时延:首帧上屏

trace name 字段 说明
DispatchTouchEvent type 字段:0=按下、1=抬起、2=移动 触摸事件分发。
点击场景下通常只有一对 down/up,中间可能有少量 move。如果 trace 中有多段点击,每对 down/up 对应一次点击。

DevEco Profiler 中点击窗口的 trace 示意

触摸起点(点击场景取 type=1 离手、触摸/滑动场景取 type=0 按下)开始,向后查找首个完整的渲染帧链路:Animator::BeginFrameGPURasterizer::DrawRSMainThread::DoCompositionRSHardwareThread::CommitAndReleaseLayers。首帧到达 CommitAndReleaseLayers 的时刻即为响应时延终点

3.2 完成时延:首屏稳定

从首帧上屏后继续向后查看,直到帧渲染活动趋于稳定——即 Animator::BeginFrameGPURasterizer::Draw 不再连续出现,或帧间隔恢复正常刷新周期。最后一帧上屏的时刻即为完成时延终点

响应时延由多个环节串联组成,需要逐段度量耗时,找出最长的瓶颈环节:

环节 起止 trace 高耗时含义 下一步深挖方向
多模输入 没有对应的trace_name,但是有对应的线程mmi_service 系统侧触摸事件 不涉及
触摸事件分发 mmi_serviceDispatchTouchEvent type=1/0 系统侧分发触摸事件到应用接收 不涉及
事件分发 DispatchTouchEvent type=1Engine::DispatchPointerDataPacket 系统层事件分发延迟 检查 touchEventDispatch、系统侧事件积压
Vsync 等待 Engine::DispatchPointerDataPacketVsyncFireCallback 触摸发生在两个 Vsync 之间,需等下一个 正常现象;若 >1 帧预算则异常
UI 线程处理 VsyncFireCallbackAnimator::BeginFrame → 完成 build/layout/paint Dart 层 build 过重或被同步重活阻塞 检查 begin_frame_ / draw_frame_,确认 BUILD/LAYOUT/PAINT 哪个阶段最重
Raster 栅格化 GPURasterizer::Draw 绘制过重或 shader/glyph 冷编译 检查 CreateGlyphAtlasLoad/Compile ShadersSurfaceFrame::Submit
应用→渲染服务 SurfaceFrame::SubmitRSMainThread::ProcessCommandUni[pid,seq] 引擎到渲染服务的提交与调度延迟 用 transactionFlag↔pid,seq 对齐、查 buffer 可用性
合成上屏 RSMainThread::DoCompositionCommitAndReleaseLayers 系统层合成负载重 检查合成器层级、fence 等待

多数情况下瓶颈在 UI 线程处理段(build 过重或同步重活阻塞)或 Raster 栅格化段(shader/glyph 冷编译)。事件分发和 Vsync 等待通常较短,若这两段长则属于系统层问题。

3.3 UI 线程瓶颈

在收窄窗口内,按 UI 线程 tid 过滤 slices,重点关注 BUILDLAYOUTPAINTAnimateBeginFramePlatformConfiguration 等名称的 slice,确认哪个阶段最重。

如果上述 slices 中缺少 BUILD/LAYOUT/PAINT,说明 trace 未启用 --trace-systrace,UI 线程归因有限。需在 FlutterLoader.ets 中添加 --trace-systrace 引擎参数后重新采集。

根因1:主线程做同步重活

现象

点击页面后白屏/卡死一会儿,然后内容一次性出来。完成时延很长。

原因

Flutter 的 UI 线程负责渲染,如果同步解析大 JSON、读文件、做复杂计算,会阻塞后续帧的生成,也会让点击的首帧迟迟上不来。

修复

把重活放到 Isolate 里:

import 'package:flutter/foundation.dart';

Future<List<Item>> loadItems() async {
  final raw = await rootBundle.loadString('assets/items.json');
  return compute(_parseItems, raw); // compute 把解析放到后台 Isolate
}

List<Item> _parseItems(String raw) {
  final list = (jsonDecode(raw) as List).cast<Map<String, dynamic>>();
  return list.map(Item.fromJson).toList();
}
案例

两案例用 1 亿次整数循环模拟"主线程同步重活"(约 0.5~2s),按钮点击触发计算,对比同步阻塞与 Isolate 后台计算的点击响应差异。

问题案例:同步阻塞 UI 线程

点击按钮后直接在主线程同步执行 1 亿次循环,loading 指示器来不及渲染,按钮卡在按下状态直到计算结束:

const _heavyIterations = 100000000; // 1 亿次循环,约 0.5~2s

// 顶层函数,供 compute() 在后台 Isolate 中调用
int _heavyWork(int iterations) {
  var result = 0;
  for (var i = 0; i < iterations; i++) {
    result = (result + i * 31) & 0x7FFFFFFF;
  }
  return result;
}

class _SyncBadTabState extends State<_SyncBadTab> {
  int? _result;

  void _run() {
    // 同步阻塞:loading 指示器不会出现,按钮卡在按下状态
    final result = _heavyWork(_heavyIterations);
    setState(() => _result = result);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              _result == null ? '点击按钮开始计算' : '计算结果:$_result',
              style: const TextStyle(fontSize: 18),
            ),
            const SizedBox(height: 16),
            ElevatedButton(onPressed: _run, child: const Text('同步计算(阻塞 UI)')),
            const SizedBox(height: 8),
            const Text('点击后界面会卡死,直到计算完成', style: TextStyle(color: Colors.red)),
          ],
        ),
      ),
    );
  }
}

修复案例:compute 放到后台 Isolate

compute() 将计算丢到后台 Isolate,主线程立即返回,loading 指示器正常渲染,界面保持响应:

class _SyncGoodTabState extends State<_SyncGoodTab> {
  int? _result;
  bool _loading = false;

  Future<void> _run() async {
    setState(() => _loading = true);
    final result = await compute(_heavyWork, _heavyIterations);
    setState(() {
      _result = result;
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              _result == null ? '点击按钮开始计算' : '计算结果:$_result',
              style: const TextStyle(fontSize: 18),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _loading ? null : _run,
              child: _loading
                  ? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2))
                  : const Text('Isolate 计算(不阻塞)'),
            ),
            const SizedBox(height: 8),
            const Text('点击后立即出现 loading,界面保持响应', style: TextStyle(color: Colors.green)),
          ],
        ),
      ),
    );
  }
}

效果对比

问题案例 修复案例
计算位置 主线程同步执行 后台 Isolate(compute
点击后首帧 卡在按下状态,loading 不出现 立即出现 loading 指示器
界面响应 完全卡死至计算结束 始终可交互
完成时延 长(计算 + 渲染串行) 短(计算与渲染并行)
hitrace截图

根因2:首屏一次性构建/加载太多

现象

页面打开后先白屏/转圈,然后内容一次性全部出现,完成时延高。

原因

首帧需要 build 大量 Widget、加载全部数据、解码所有图片。构建越重,首帧来得越慢,响应时延和完成时延都会被拉长。

修复
  • 优先显示骨架屏或占位,先给用户反馈;
  • 数据分页、图片懒加载;
  • 非首屏内容延迟 build。
案例

两案例的 Tab 页各有一个按钮,点击后跳转到列表页面。完成时延从按钮点击开始度量,到列表内容全部渲染稳定为止。对比非懒加载全量构建与骨架屏 + 分批懒加载的完成时延。

问题案例:非懒加载 ListView,500 个 Widget 一次性全量构建

Tab 页展示一个跳转按钮;点击后 Navigator.push 进入 _OverBuildBadListPage,该页面在 initState 中生成 500 条数据,build 返回非懒加载 ListView,500 个 Widget 全量构建,首帧需等待全部 build 完成:

class _OverBuildBadTab extends StatelessWidget {
  const _OverBuildBadTab();
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ElevatedButton(
            onPressed: () => Navigator.of(context).push(
              MaterialPageRoute(builder: (_) => const _OverBuildBadListPage()),
            ),
            child: const Text('打开列表页面(非懒加载 500 项)'),
          ),
          const SizedBox(height: 8),
          const Text('点击后白屏,500 项一次性全量构建', style: TextStyle(color: Colors.red)),
        ],
      ),
    );
  }
}

class _OverBuildBadListPageState extends State<_OverBuildBadListPage> {
  late final List<String> _items;

  @override
  void initState() {
    super.initState();
    _items = List.generate(500, (i) => 'Item $i');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('问题案例')),
      body: ListView(children: _items.map(_buildListItem).toList()),
    );
  }
}

修复案例:骨架屏 + 分批懒加载

Tab 页同样展示一个跳转按钮;点击后进入 _OverBuildGoodListPage,该页面先渲染骨架屏占位,让首帧快速上屏;再分 10 批、每批 50 条增量加载,用 ListView.builder 懒加载:

class _OverBuildGoodTab extends StatelessWidget {
  const _OverBuildGoodTab();
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ElevatedButton(
            onPressed: () => Navigator.of(context).push(
              MaterialPageRoute(builder: (_) => const _OverBuildGoodListPage()),
            ),
            child: const Text('打开列表页面(骨架屏 + 分批加载)'),
          ),
          const SizedBox(height: 8),
          const Text('点击后骨架屏先上屏,内容逐步填充', style: TextStyle(color: Colors.green)),
        ],
      ),
    );
  }
}

class _OverBuildGoodListPageState extends State<_OverBuildGoodListPage> {
  final List<String> _items = [];
  bool _loading = true;

  @override
  void initState() {
    super.initState();
    _loadIncrementally();
  }

  Future<void> _loadIncrementally() async {
    await Future.delayed(const Duration(milliseconds: 16)); // 先让骨架帧渲染
    for (int batch = 0; batch < 10; batch++) {
      if (!mounted) return;
      setState(() {
        _items.addAll(List.generate(50, (i) => 'Item ${batch * 50 + i}'));
        _loading = false;
      });
      await Future.delayed(Duration.zero); // 让出事件循环,不添加额外延迟
    }
  }

  @override
  Widget build(BuildContext context) {
    final body = _loading
        ? ListView.builder(itemCount: 10, itemBuilder: (_, __) => _buildSkeletonItem())
        : ListView.builder(itemCount: _items.length, itemBuilder: (_, i) => _buildListItem(_items[i]));
    return Scaffold(
      appBar: AppBar(title: const Text('修复案例')),
      body: body,
    );
  }
}

补充:_buildListItem 每项含头像、标题、副标题和 2~5 个标签,构建开销远大于纯文本项;_buildSkeletonItem 返回灰色圆角占位 Container。两案例共用这两个辅助函数,仅加载策略不同。修复案例分批间用 Future.delayed(Duration.zero) 让出事件循环,不添加额外延迟,使完成时延主要由实际构建耗时决定。

效果对比

问题案例 修复案例
跳转方式 按钮点击 → Navigator.push 按钮点击 → Navigator.push
每项复杂度 头像+标题+副标题+标签 头像+标题+副标题+标签
首帧构建 500 个复杂项全量 build 10 个骨架项
ListView 类型 ListView(children:) 非懒加载 ListView.builder 懒加载
首屏反馈 白屏等待全部 build 完成 骨架屏立即上屏
数据加载 一次性 500 条 分 10 批 × 50 条增量,批间无额外延迟
完成时延 长(500 项首帧阻塞) 短(首帧快,内容逐步填充)
hitrace截图

根因3:setState 粒度太大,整页重建

现象

点击后页面迟迟没有反馈,或者刷新一下要等很久内容才稳定。

原因

setState 会触发所在 Widget 及其子树的 rebuild。如果它放在页面根节点,整页都会重走 Build/Layout/Paint,首帧(也就是用户点击后的第一帧反馈)自然来得慢。

修复

把状态下放到最小范围,静态子树用 const 避免重复构建。

// ❌ 不好:计数器变了,HeavyHeader 和 HeavyList 被迫重画
class BadPage extends StatefulWidget {
  const BadPage({super.key});
  @override
  State<BadPage> createState() => _BadPageState();
}

class _BadPageState extends State<BadPage> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        HeavyHeader(),                 // 没变,却被迫 rebuild
        Expanded(child: HeavyList()),  // 没变,却被迫 rebuild
        Text('$_count'),
        ElevatedButton(
          onPressed: () => setState(() => _count++),
          child: const Text('+1'),
        ),
      ],
    );
  }
}
// ✅ 好:只有 Counter 自己 rebuild,静态部分全部 const
class GoodPage extends StatelessWidget {
  const GoodPage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        HeavyHeader(),               // const:Flutter 会直接复用,不再 rebuild
        Expanded(child: HeavyList()),
        Counter(),                   // 变化范围被限制在这个子树
      ],
    );
  }
}

class Counter extends StatefulWidget {
  const Counter({super.key});
  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Text('$_count'),
        ElevatedButton(
          onPressed: () => setState(() => _count++),
          child: const Text('+1'),
        ),
      ],
    );
  }
}
案例

四个 Tab 对比两种重量级内容的重建开销:非懒加载Wrap + 300 项,所有子项均有 Element)和懒加载GridView.builder + 500 项,仅可见项有 Element)。每个 Tab 下方均有计数器按钮,+1 时触发 setState

Tab 重量级内容 setState 位置 +1 后重建范围
问题·非懒加载 Wrap(300) 页面根 300 项全部重建
修复·非懒加载 const Wrap(300) _ScopedCounter 仅计数器
问题·懒加载 GridView.builder(500) 页面根 仅可见项重建
修复·懒加载 const GridView.builder(500) _ScopedCounter 仅计数器

问题案例:setState 在页面根,重量级静态子树被迫重建

_SetStateBadTab 是 StatefulWidget,setState 在页面根节点。+1 时不仅计数器重建,上方的重量级子树也被迫 rebuild。useLazy 参数切换非懒加载与懒加载:

class _SetStateBadTab extends StatefulWidget {
  final bool useLazy;
  const _SetStateBadTab({this.useLazy = false});
  @override
  State<_SetStateBadTab> createState() => _SetStateBadTabState();
}

class _SetStateBadTabState extends State<_SetStateBadTab> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: widget.useLazy
              ? const _HeavyStaticContentLazy()
              : const _HeavyStaticContentNonLazy(),
        ),
        Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              Text('$_count', style: const TextStyle(fontSize: 24)),
              const SizedBox(width: 16),
              ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+1')),
            ],
          ),
        ),
      ],
    );
  }
}

修复案例:静态子树 const,状态下放到最小范围

页面改为 StatelessWidget,静态子树全部 const。计数器状态下沉到独立的 _ScopedCounter,+1 时只有它自己 rebuild:

class _SetStateGoodTab extends StatelessWidget {
  final bool useLazy;
  const _SetStateGoodTab({this.useLazy = false});
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: widget.useLazy
              ? const _HeavyStaticContentLazy()
              : const _HeavyStaticContentNonLazy(),
        ),
        const Padding(padding: EdgeInsets.all(16), child: _ScopedCounter()),
      ],
    );
  }
}

class _ScopedCounterState extends State<_ScopedCounter> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Text('$_count', style: const TextStyle(fontSize: 24)),
        const SizedBox(width: 16),
        ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+1')),
      ],
    );
  }
}

补充:两版重量级子树共用 _buildHeavyCell(图标+文本)。非懒加载 _HeavyStaticContentNonLazySingleChildScrollView + Wrap(300项),所有子项均有 Element,+1 时全部重建(约 1800 个 Widget 实例,耗时 ~15-20ms)。懒加载 _HeavyStaticContentLazyGridView.builder(500项),仅可见项有 Element,+1 时仅重建约 20 个可见项(<1ms)。修复案例中两者均标记 const,Flutter 直接复用已有实例,+1 时不再 rebuild。

效果对比

问题·非懒加载 修复·非懒加载
重量级内容 Wrap(300项) const Wrap(300项)
+1 后重建 300 项全部重建 仅计数器
+1 重建开销 ~15-20ms <1ms
响应时延 长(可见 jank)
hitrace截图

3.4 Raster 线程瓶颈

在收窄窗口内,按 Raster 线程 tid 过滤 Flutter/Impeller 相关 slices,重点观察以下 slice 是否出现及其耗时:

slice 名称 指向的根因
impeller::CreateGlyphAtlas / UpdateAtlasBitmap 字形图集冷启动(首次渲染新字形)
Load/Compile Shaders / bisheng_graphic_pipeline_compilation 着色器冷编译(首次使用新 shader)
SurfaceFrame::Submit / Encode 内部无子瓶颈 场景复杂、绘制指令过多
AcquireNextImageKHR / vkQueueSubmit 耗时长 GPU 资源竞争或驱动瓶颈

其他常见 Raster 瓶颈(大图解码、saveLayer 过度绘制、阴影模糊等)。

根因1:大图按原尺寸解码

现象

点进页面后图片迟迟不显示,或下拉刷新后内容稳定很慢。

原因

图片默认按原图分辨率解码,如果原图 4K 但只显示 200×200,会浪费解码时间与显存,拉长完成时延。

修复

按目标显示尺寸解码:

Image.network(
  imageUrl,
  cacheWidth: 200,  // 只解码到 200×200
  cacheHeight: 200,
);
案例

两案例的 Tab 页各有一个按钮,点击后跳转到图片列表页面。完成时延从按钮点击开始度量,到图片全部解码并显示为止。图片数据在进入页面时由 dart:ui 运行时生成(4000×3000 PNG),无网络依赖,确保只对比解码耗时。

问题案例:无 cacheWidth/cacheHeight,按 4000×3000 原尺寸解码

Tab 页展示一个跳转按钮;点击后 Navigator.push 进入 _ImageBadListPageImage.memory 未指定 cacheWidth/cacheHeight,图片按 4000×3000 原分辨率解码,但最终只显示 200 高——解码与显存开销全部浪费:

class _ImageBadTab extends StatelessWidget {
  final Uint8List imageBytes;
  const _ImageBadTab({required this.imageBytes});
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ElevatedButton(
            onPressed: () => Navigator.of(context).push(
              MaterialPageRoute(builder: (_) => _ImageBadListPage(imageBytes: imageBytes)),
            ),
            child: const Text('加载图片(原尺寸解码)'),
          ),
          const SizedBox(height: 8),
          const Text('4000×3000 原尺寸解码,Raster 线程耗时长', style: TextStyle(color: Colors.red)),
        ],
      ),
    );
  }
}

class _ImageBadListPage extends StatelessWidget {
  final Uint8List imageBytes;
  const _ImageBadListPage({required this.imageBytes, super.key});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('问题案例')),
      body: ListView.builder(
        itemCount: 3,
        itemBuilder: (context, index) => Padding(
          padding: const EdgeInsets.all(8),
          child: Image.memory(imageBytes, height: 200, fit: BoxFit.cover),
        ),
      ),
    );
  }
}

修复案例:指定 cacheWidth/cacheHeight,只解码到显示所需尺寸

Tab 页同样展示一个跳转按钮;点击后进入 _ImageGoodListPage,指定 cacheWidth: 800cacheHeight: 600(物理像素),解码到 800×600 即可,解码时间与显存大幅减少:

class _ImageGoodTab extends StatelessWidget {
  final Uint8List imageBytes;
  const _ImageGoodTab({required this.imageBytes});
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ElevatedButton(
            onPressed: () => Navigator.of(context).push(
              MaterialPageRoute(builder: (_) => _ImageGoodListPage(imageBytes: imageBytes)),
            ),
            child: const Text('加载图片(缩放解码)'),
          ),
          const SizedBox(height: 8),
          const Text('800×600 缩放解码,Raster 线程耗时短', style: TextStyle(color: Colors.green)),
        ],
      ),
    );
  }
}

class _ImageGoodListPage extends StatelessWidget {
  final Uint8List imageBytes;
  const _ImageGoodListPage({required this.imageBytes, super.key});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('修复案例')),
      body: ListView.builder(
        itemCount: 3,
        itemBuilder: (context, index) => Padding(
          padding: const EdgeInsets.all(8),
          child: Image.memory(
            imageBytes,
            height: 200,
            fit: BoxFit.cover,
            cacheWidth: 800, // 只解码到 800×600(物理像素)
            cacheHeight: 600,
          ),
        ),
      ),
    );
  }
}

补充:图片数据在进入页面时由 dart:ui 运行时生成(PictureRecorder + Canvas 画渐变+500 个圆 → toImagetoByteData(png)),生成一次后缓存为 Uint8List,两案例共用同一份字节。cacheWidth/cacheHeight 的单位是物理像素,若设备 DPR=3,逻辑像素 200 高对应物理像素 600,此处取 800 略有富余以保证清晰度。

效果对比

问题案例 修复案例
跳转方式 按钮点击 → Navigator.push 按钮点击 → Navigator.push
图片来源 运行时生成 4000×3000 PNG 同一份字节
解码 API Image.memory Image.memory + cacheWidth/cacheHeight
解码分辨率 4000×3000(原尺寸) 800×600
显示尺寸 200 高 200 高
解码像素量 1200 万 48 万(约 1/25)
显存占用
完成时延 长(大图解码慢) 短(解码快,内容尽快稳定)
hitrace截图

4. 参考与延伸

Logo

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

更多推荐