ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flutter OHOS 页面滑动卡顿与掉帧问题介绍

Flutter OHOS 页面滑动卡顿与掉帧问题介绍 1. 概述用户反馈页面卡时最常见的一类是滑动或动画卡顿画面不连贯、一顿一顿。这在 Flutter 里通常对应掉帧Jank——某一帧生成耗时超过帧预算屏幕只能继续显示上一帧。60Hz表示屏幕每秒刷新 60 次相邻两次刷新间隔约16.6ms90Hz约11.1ms120Hz约8.3ms。2. Flutter 的渲染管线介绍Flutter 渲染一帧大致经过四个阶段阶段做什么大致耗在哪条线程Build根据状态变化构建/更新 Widget 树、Element 树UI 线程主线程LayoutRenderObject 树计算每个节点的大小和位置UI 线程Paint生成绘制指令构建 Layer treeUI 线程Composite将 Layer tree 栅格化成像素输出到屏幕Raster 线程渲染线程/GPU 线程其中UI 线程运行 Dart VM执行你的 Dart 代码和 Flutter 框架代码产出 Layer treeRaster 线程接收 Layer tree调用 Skia 或 Impeller 进行栅格化与合成最终送屏。性能分析时两条线程的时间都要看UI 线程耗时过长说明 Dart 层有性能问题Raster 线程耗时过长说明绘制太重阴影、模糊、大图、saveLayer 等。后续主要分析都围绕 UI 线程和 Raster 线程展开需要先学会通过trace确认它们的 tid。通过查看 trace 的线程清单重点关注以下角色分类角色典型线程名说明flutter_ui1.ui、root.1.ui、root.N.uiFlutter UI 线程运行 Dart 代码platform_main应用包名如.my_app应用主线程flutter_raster1.raster、root.1.rasterFlutter Raster 线程执行光栅化Flutter 3.35 线程合并从 Flutter 3.35 版本起OHOS 平台上 UI 线程与应用主线程可能合并为同一条线程线程名为应用包名如.complex_layout被归类为platform_main而非flutter_ui。此时不能仅靠线程名判断 UI 线程而应通过搜索flutter::Animator::BeginFrametrace 来定位——该 slice 所在的 tid 即为 UI 线程。3. 基于 OHOS平台Trace 的滑动丢帧定位渲染链路概览下面这条链路展示了“手指触屏 → Flutter 处理 → 系统合成 → 屏幕上屏”的完整过程。每个 trace 对应其中一个关键节点便于快速理解全貌。可控性提示链路中大部分事件由系统或 Flutter 框架自动产生应用代码无法直接改动只有Animator::BeginFrame对应 Dart 层 BUILD/LAYOUT/PAINT和GPURasterizer::Draw对应 Raster 线程绘制负载的耗时会被应用代码的复杂度间接影响是性能优化的主要发力点。trace name作用所属线程/模块可控性 / 说明originEventHandle或者不存在系统收到手指触摸/滑动事件的原始输入系统的多模输入子系统mmi_service系统事件不可改动仅用于定位输入起点DispatchTouchEventid:N, pointXXXX pointYXXX typex系统将触摸事件派发给应用主线程type 0-按下 1-抬起 2-移动应用主线程/应用包名系统事件不可改动Shell::OnPlatformViewDispatchPointerDataPacketFlutter Shell 层收到平台视图传来的触摸数据包应用主线程/应用包名Flutter 框架事件不可改动Engine::DispatchPointerDataPacketFlutter 引擎将触摸数据包分发到 Dart 层处理UI 线程Flutter 框架事件不可改动flutter::VsyncFireCallback收到垂直同步信号通知 Flutter 可以开始渲染下一帧OS_VSyncThread系统 VSync 信号不可改动Animator::BeginFrameFlutter 动画器开始一帧的构建、布局、绘制UI 线程应用代码可优化减少 rebuild、layout、paint 开销GPURasterizer::Draw将 UI 线程生成的 Layer tree 栅格化为像素Raster 线程应用绘制负载可优化避免 saveLayer、重阴影、大图、过度模糊oh_flutter_1SurfaceFlutter 向系统提交当前帧的图形 bufferFlutter → RenderService框架事件不可改动用于观察 buffer 生产/消费是否匹配RSMainThread::DoCompositionRenderService 对多个窗口/图层进行合成render_service系统合成事件不可改动RenderFrameGPU 执行真正的像素绘制GPUGPU 执行不可改动RSHardwareThread::CommitAndReleaseLayers把合成好的图层提交给显示硬件释放过期 bufferrender_service / 显示硬件系统/硬件事件不可改动3.1 识别滑动过程分析滑动丢帧的前提是先确定哪一段 trace 是滑动。DevEco Profiler 抓取的 trace 通常长达数秒到数十秒包含应用启动、空闲、滑动、停止等多个阶段。只有先把滑动区间圈出来后续的丢帧定位才有意义。flutter::APP_LIST_FLING是应用代码主动埋点的滑动生命周期标记。如果 trace 中存在该 slice它覆盖了手指拖滑和惯性滑动的完整过程。在 trace 中搜索APP_LIST_FLING取其 start 和 end 时间戳直接作为后续丢帧分析的时间窗口。注意APP_LIST_FLING包含手指拖滑和惯性滑动全过程。但在特殊情况下该标记可能未完全覆盖实际滑动过程如滑动开始前埋点未触发、或滑动结束后惯性仍持续此时需要结合触摸事件链扩展窗口。3.2 识别丢帧位置3.2.1 定位丢帧在滑动窗口内定位丢帧。有两种方法优先使用第一种方法一通过flutter::SceneDisplayLag快速定位Flutter 提供了flutter::SceneDisplayLagtrace 来标识渲染管线中出现的丢帧。在滑动窗口内搜索该 trace可以直接定位丢帧发生的时间点无需逐帧对比耗时。方法二通过render_service 快速查阅render_service 提供了 两个trace泳道来说明buffer消费情况。收藏两个泳道快速查阅两个泳道不重合或空隙的时间点。泳道说明oh_flutter_1Surfaceflutter提交的生产buffer及其数量。名字通常是oh_flutter_开头数字Surface结尾render_service消费flutter提交的buffer3.2.2 判断瓶颈点ui线程和raster线程耗时长短没有一个基准根据慢帧的指标分布判断瓶颈在哪条线程指标特征瓶颈线程下一步深挖方向ui远大于rasterUI 线程检查Animator::BeginFrame→begin_frame_/draw_frame_确认 BUILD/LAYOUT/PAINT 哪个阶段最重raster远大于uiRaster 线程检查GPURasterizer::Draw子调用栈确认是 shader 编译、图片解码、saveLayer 还是 fence 等待ui和raster都高连锁反应通常是 UI 层重建过大拖累 Raster先治 UI 线程ui/raster都小但存在丢帧UI 线程或主线程检查DartIsolate::HandleMessage、Animator::AwaitVSync、fence 等待、binder 阻塞锁定最慢帧后确认两个线程的帧号frame_number:是否相同。以其时间戳为中心收窄窗口分别查看 UI 线程和 Raster 线程在该窗口内的 slices找到最耗时的 slice。根据耗时和调用栈信息判断是 UI 线程还是 Raster 线程的瓶颈。标记trace name说明起点flutter::Animator::BeginFrameUI线程绘制终点flutter::GPURasterizer::DrawRaster线程光栅化主要分支一UI 线程瓶颈在收窄窗口内按 UI 线程 tid 过滤 slices重点关注BUILD、LAYOUT、PAINT、Animate、BeginFrame、PlatformConfiguration等名称的 slice确认哪个阶段最重。slice 名称说明指向的根因BUILDWidget 树的构建阶段列表项构建复杂、itemBuilder中创建重量级 Widget、setState触发大范围重建、动画导致每帧 rebuildLAYOUTRenderObject 树的布局阶段列表不定高参见4.1 根因ListView 不定高、嵌套复杂 Flex/Stack、使用IntrinsicHeight/IntrinsicWidth等强制固有尺寸测量PAINT生成绘制指令绘制指令复杂、大量裁剪/复杂CustomPaint、子树未用RepaintBoundary隔离导致重绘扩散Animate动画回调执行动画回调中做重计算、AnimatedBuilder未复用child、每帧setState重建大量 WidgetPlatformConfiguration平台配置/消息处理Platform Channel 同步调用阻塞、大量平台消息集中处理BeginFrame一帧开始调度 BUILD/LAYOUT/PAINT 各阶段Dart 主线程被其他消息阻塞如DartIsolate::HandleMessage、同步耗时操作、主线程与 Raster 线程 sync 等待图片注意事项截图应展示 UI 线程在慢帧窗口内的调用栈展开标注 BUILD/LAYOUT/PAINT 各阶段耗时。如未开启--trace-systrace则标注缺少这些阶段。应用开发者可以通过flutter内置工具devtools细看耗时原因。主要分支二Raster 线程瓶颈在收窄窗口内按 Raster 线程 tid 过滤 Flutter/Impeller 相关 slices重点观察以下 slice 是否出现及其耗时slice 名称说明指向的根因impeller::CreateGlyphAtlas/UpdateAtlasBitmap字形图集的创建与更新字形图集冷启动首次渲染新字形Load/Compile Shaders/bisheng_graphic_pipeline_compilation着色器加载与编译着色器冷编译首次使用新 shaderSurfaceFrame::Submit/Encode帧的提交与编码场景复杂、绘制指令过多AcquireNextImageKHR/vkQueueSubmitGPU 图像获取与命令队列提交GPU 资源竞争或驱动瓶颈4 常见根因和修复方案4.1 根因ListView 不定高现象长列表快速滑动时掉帧低端机尤其明显。Flutter 不知道每一项的高度滚动时每出现一项都要重新测量itemBuilder会被高频调用测量成本被放大。Trace 判定信号在滑动窗口内若 UI 线程成为瓶颈且出现以下特征高度怀疑是 ListView 不定高trace 特征说明UI 线程LAYOUT阶段占比明显高于BUILD/PAINT每项进入视口时都要测量高度布局耗时被放大调用栈中出现RenderSliverList.performLayout/childLayout使用的是变高列表实现逐项测量子节点未出现RenderSliverFixedExtentList未通过itemExtent/prototypeItem固定高度快速滑动时BUILDLAYOUT密集交替出现新 item 不断进入视口每次都要 build layout 测高注意trace 只能给出高度怀疑信号无法直接显示“未设置itemExtent”。最终根因需结合代码确认。代码验证打开对应页面源码检查ListView.builder/ListView.separated等是否缺少以下任一属性itemExtent固定高度prototypeItem提供一条高度样板若二者均未设置且列表项高度确实随内容变化则可确认本根因。修复固定高度直接给itemExtentListView.builder( itemCount: data.length, itemExtent:56,// 每一项高度 56跳过测量itemBuilder:(context,i) ItemCell(data[i]), );高度不固定时也可用prototypeItem提供一份样板。案例问题案例不定高逐项测量问题案例修复案例说明不定高逐项测量定高跳过测量截图Widget_buildBadListView(){ returnListView.builder( itemCount: itemCount,// 未设置 itemExtent每一项高度需实时测量itemBuilder:(context,index) _buildItem(index,fixedHeight:false), ); }未设置itemExtent时ListView 使用RenderSliverList变高列表滚动偏移无法直接计算每出现一项都要 build layout 测一次高度。标签数量 27 个、是否换行不一导致每项高度在约 99130px 之间变化快速滑动时测量成本被放大存在丢帧。修复案例定高跳过测量Widget_buildGoodListView(){ returnListView.builder( itemCount: itemCount, itemExtent:136,// 固定高度足以容纳两行标签跳过逐项测量itemBuilder:(context,index) _buildItem(index,fixedHeight:true), ); }设置itemExtent后ListView 切换为RenderSliverFixedExtentList滚动偏移 index * 136无需逐项测量即可定位可见区域。内容与问题案例完全一致只是较短项底部留白——这是固定高度换取流畅度的预期权衡。两案例共用同一个_buildItem仅mainAxisSize不同——不定高案例用.min收缩到内容 → 高度参差定高案例用.max填满itemExtentWidget _buildItem(intindex, {requiredboolfixedHeight}) { final tagCount (index%6) 2;// 2 ~ 7 个标签returnContainer( margin: _itemMargin, padding: _itemPadding, decoration: _itemDecorations[index% _itemDecorations.length], child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: fixedHeight ? MainAxisSize.max: MainAxisSize.min, children: [ Text(Item $index, style: _titleStyle),constSizedBox(height:8), Wrap( spacing:6, runSpacing:6, children: List.generate(tagCount, _buildTag), ), ], ), ); }补充itemExtent只消除了量高度定位的开销每一项进入视口时 build layout 仍会执行。为控制单项构建成本两案例还共同做了以下处理标签用轻量ContainerText替代Chip避免Chip内部Material/InkWell/shape 裁剪开销背景装饰BoxDecoration 颜色 shade 预计算为static final列表不在每次itemBuilder调用时新建样式、边距均提为static const全局共享不重建。效果对比问题案例修复案例itemExtent未设置136每项高度随标签数量/换行变化约 99~130px固定 136px滚动定位逐项 buildlayout 测高度index * 136直接定位快速滑动存在丢帧流畅hitrace截图4.2 根因过度绘制/离屏缓冲现象静态看正常滑动或动画时掉帧只有 Raster 线程耗时长。Trace 判定信号若滑动时 UI 线程正常但 Raster 线程成为瓶颈且出现以下特征高度怀疑过度绘制 / 离屏缓冲trace 特征说明GPURasterizer::Draw占比显著高于Animator::BeginFrame绘制负载重问题在 Raster 线程GPURasterizer::Draw子调用中出现saveLayer存在离屏缓冲通常是裁剪、Opacity、阴影等触发出现BackdropFilter/ImageFilter相关 slice全屏或大面积模糊导致逐像素卷积动画场景下每帧GPURasterizer::Draw都很长动画每帧都触发重绘saveLayer / 模糊成本被放大排除 shader 编译无Load/Compile Shaders说明不是着色器冷编译而是绘制本身过重注意trace 能告诉你“存在 saveLayer / 模糊 / 绘制过重”但无法直接指出是哪一个 Widget 触发的。具体 Widget 需结合代码或 DevTools 的 Repaint Rainbow 等工具定位。代码验证回到对应页面代码重点检查可见 item 及其动画子树中是否存在以下高耗操作Clip.antiAliasWithSaveLayer包括ClipRRect等使用antiAliasWithSaveLayer动画中直接使用OpacityWidget大面积BackdropFilter/ImageFiltered模糊复杂BoxShadow大blurRadius/spreadRadius缺少RepaintBoundary导致动画重绘向父级扩散修复圆角用默认裁剪避免antiAliasWithSaveLayer动画中需要透明度变化时用FadeTransition/AnimatedOpacity替代直接动画Opacity静态半透明直接改颜色 alpha少用 Opacity Widget模糊只用于小区域高频重绘的局部区域用RepaintBoundary隔离但别滥用。案例50 个 item 各自循环播放透明度动画0.3↔1.02s两案例动画参数完全相同差别只在 Widget 选型和裁剪/模糊/阴影配置。问题案例四重 saveLayer 叠加动画驱动用AnimatedBuilder 裸Opacityopacity 每帧变化触发 paint 阶段saveLayeroverrideWidgetbuild(BuildContext context){returnAnimatedBuilder( animation: _animation, builder: (context, child){returnOpacity( opacity: _animation.value, // 每帧变化 → saveLayer child: child, ); }, child: _buildBadContent(widget.index), ); }内容层还叠加了另外三个 saveLayer / 高耗操作Widget_buildBadContent(int index) {returnClipRRect(clipBehavior: Clip.antiAliasWithSaveLayer,// ① 离屏裁剪borderRadius: _overdrawClipRadius,child: Container(// ...decoration: BoxDecoration(gradient: LinearGradient(colors: ...),boxShadow: const [ BoxShadow(blurRadius:12,spreadRadius:2, ...),// ③ 大面积阴影], ),child: Stack(children: [ Positioned.fill(child: BackdropFilter(// ② 全区域高斯模糊filter: ImageFilter.blur(sigmaX:8,sigmaY:8),child: Container(color: Colors.white.withValues(alpha:0.1)), ), ), Center(child: Text(Item $index,style: _overdrawTextStyle)), ], ), ), ); }每个可见 item 每帧 OpacitysaveLayer antiAliasWithSaveLayersaveLayer 全区域BackdropFilter模糊 大面积阴影重绘Raster 线程迅速过载。修复案例逐项消除 saveLayeroverrideWidget build(BuildContext context) {returnRepaintBoundary(// ④ 隔离重绘范围child: FadeTransition(// ①→合成层 opacity不 saveLayeropacity: _animation,child: _buildGoodContent(widget.index), ), ); }Widget_buildGoodContent(int index) {returnClipRRect(clipBehavior: Clip.hardEdge,// ②→硬边裁剪不 saveLayerborderRadius: _overdrawClipRadius,child: Container(// ...decoration: BoxDecoration(gradient: LinearGradient(colors: ...),boxShadow: const [ BoxShadow(blurRadius:6,spreadRadius:0, ...),// ③→缩小阴影], ),child: Center(child: Text(Item $index,style: _overdrawTextStyle), ), ), ); }核心改动Opacity→FadeTransitionopacity 下沉到合成层OpacityLayer子树只 paint 一次之后每帧仅 compositor 调整 layer opacitypaint 阶段不再saveLayerantiAliasWithSaveLayer→hardEdge直接在 paint 时跳过裁剪区域外像素不开离屏 bufferBackdropFilter直接移除背景只是渐变模糊无实际视觉收益删掉省掉逐像素卷积BoxShadow缩小blur 12→6, spread 2→0减少阴影重绘面积外层RepaintBoundary动画只脏标记自己的 layer不向父级 ListView 传播。补充AnimatedBuilder的child参数应传入不变的内容如上_buildBadContent而非在 builder 内部每次调用——这样 Widget tree 只构建一次。问题案例虽仍因Opacity触发 saveLayer但至少避免了每帧重建子树的开销。效果对比开销来源问题案例修复案例透明度动画Opacity→ paint 阶段 saveLayerFadeTransition→ 合成层调 opacity圆角裁剪antiAliasWithSaveLayer→ 离屏 bufferhardEdge→ 直接裁剪背景模糊全区域BackdropFiltersigma8移除阴影blur 12 / spread 2blur 6 / spread 0重绘隔离无显式RepaintBoundary有RepaintBoundary快速滑动Raster 线程红明显掉帧流畅hitrace截图5. 参考与延伸Flutter 官方性能文档https://docs.flutter.dev/perfDevTools Performance 指南https://docs.flutter.dev/tools/devtools/performance鸿蒙 性能调优工具简介https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-insight-description鸿蒙 trace 级深挖配套三篇性能分析第一步-梳理线程顺序性能分析-帧渲染跟踪性能分析-滑动响应时延
返回列表