写点什么

复杂业务如何保证 Flutter 的高性能高流畅度?

  • 2020-04-28
  • 本文字数:4393 字

    阅读完需:约 14 分钟

复杂业务如何保证Flutter的高性能高流畅度?

背景

高性能高流畅度一直是 Flutter 团队宣传的一大亮点,也是当初闲鱼选择 Flutter 的重要因素之一,但是随着复杂业务的应用落地,通过 Flutter 页面和原生页面滑动流畅度对比,我们开始产生怀疑,因为部分 Flutter 页面流畅度明显低于 Native,是 Flutter 的宣传言过其实还是我们开发人员使用姿势有问题,今天我们就来具体分析下。

Flutter 渲染原理简介

优化之前我们先来介绍下 Flutter 的渲染原理,通过这部分基础了解渲染流程以及主要耗时花费。


Flutter 视图树包含了三颗树:Widget、Element、RenderObject


  • Widget: 存放渲染内容、它只是一个配置数据结构,创建是非常轻量的,在页面刷新的过程中随时会重建

  • Element: 同时持有 Widget 和 RenderObject,存放上下文信息,通过它来遍历视图树,支撑 UI 结构

  • RenderObject: 根据 Widget 的布局属性进行 layout,paint ,负责真正的渲染


从创建到渲染的大体流程是:根据 Widget 生成 Element,然后创建相应的 RenderObject 并关联到 Element.renderObject 属性上,最后再通过 RenderObject 来完成布局排列和绘制。


例如下面这段布局代码


Container(      color: Colors.blue,      child: Row(        children: <Widget>[Image.asset('image'),Text('text'),],),);
复制代码


对应三棵树的结构如下图



了解了这三棵树,我们再来看下页面刷新的时候具体做了哪些操作


当需要更新 UI 的时候,Framework 通知 Engine,Engine 会等到下个 Vsync 信号到达的时候,会通知 Framework 进行 animate, build,layout,paint,最后生成 layer 提交给 Engine。Engine 会把 layer 进行组合,生成纹理,最后通过 Open Gl 接口提交数据给 GPU, GPU 经过处理后在显示器上面显示,如下图:



结合前面的例子,如果 text 文本或者 image 内容发生变化会触发哪些操作呢?


Widget 是不可改变,需要重新创建一颗新树,build 开始,然后对上一帧的 element 树做遍历,调用他的 updateChild,看子节点类型跟之前是不是一样,不一样的话就把子节点扔掉,创造一个新的,一样的话就做内容更新,对 renderObject 做 updateRenderObject 操作,updateRenderObject 内部实现会判断现在的节点跟上一帧是不是有改动,有改动才会别标记 dirty,重新 layout、paint,再生成新的 layer 交给 GPU,流程如下图:



到这里大家对 Flutter 在渲染方面有基本的理解,作为后面优化部分内容理解的基础。

性能分析工具及方法

下面来看下性能分析工具,注意,统计性能数据一定要在真机+profile 模式下运行,拿到最接近真实的体验数据。


performance overlay


平时常用的性能分析工具有 performance overlay,通过他可以直观看到当前帧的耗时,但是他是 UI 线程和 GPU 线程分开展示的,UI Task Runner 是 Flutter Engine 用于执行 Dart root isolate 代码,GPU Task Runner 被用于执行设备 GPU 的相关调用。绿色的线表示当前帧,出现红色则表示耗时超过 16.6ms,也就是发生丢帧现象



Dart DevTool


另一个工具是 Dart DevTool ,就是早期的 Observatory,官方提供的性能检测工具。它的 timeline 界面可以让逐帧分析应用的 UI 性能。但是目前还是预览版,存在一些问题。


profile 模式下运行起来,点击 android studio 底部的菜单按钮,会弹出一个网页



点击顶部的 Timeline 菜单



这个时候滑动页面,每一帧的耗时会以柱形 bar 的形式显示在页面上,每条 bar 代表一个 frame,同时用不同颜色区分 UI/GPU 线程耗时,这个时候我们要分析卡顿的场景就需要选中一条红色的 bar(总耗时超过 16.6ms),中间区域的 Frame events chart 显示了当前选中的 frame 的事件跟踪,UI 和 GPU 事件是独立的事件流,但它们共享一个公共的时间轴。


选中 Frame events chart 中的某个事件,以上图为例 Layout 耗时最长,我们选中它,会在底部 Flame chart 区域显示一个自顶向下的堆栈跟踪,每个堆栈帧的宽度表示它消耗 CPU 的时长,消耗大量 CPU 时长的堆栈是我们首要分析的重点,后面就是具体分析堆栈,定位卡顿问题。


debug 调试工具


另外还有一些 debug 调试工具可以辅助查看更多信息,注意,只能在 debug 模式下使用分析,拿到的数据不能作为性能标准


debugProfileBuildsEnabled:向 Timeline 事件中添加每个 widget 的 build 信息


debugProfilePaintsEnabled:向 timeline 事件中添加每个 renderObject 的 paint 信息


debugPaintLayerBordersEnabled:每个 layer 会出现一个边框,帮助区分 layer 层级


debugPrintRebuildDirtyWidgets:打印标记为 dirty 的 widgets


debugPrintLayouts:打印标记为 dirty 的 renderObjects


debugPrintBeginFrameBanner/debugPrintEndFrameBanner:打印每帧开始和结束


实例分析


了解这些工具下面我们来看个简单的 demo 具体分析下,一个由 Column、Container、ListView 嵌套的布局,其中有个定时器控制 Text 中显示的文本实时更新


{@overrideState<StatefulWidget> createState() {  return _TestDemoState();}}class _TestDemoState extends State<TestDemo> {int _count = 0;Timer _timer;...@overrideWidget build(BuildContext context) {  return new Scaffold(      appBar: new AppBar(        title: new Text("Test Demo"),      ),      body: content()  );}Widget content(){  Widget result = Column(    children: <Widget>[      Container(...),      Container(...),      Container(...),      Container(          ...          child: Center(            child: Text(              _count.toString(),            ),)), ],  );  return result;}}
复制代码



大部分 widget 都是静态的,只有黄色 Container 中包含一个内容一直刷新的 Text,这个时候我们打开 debugProfileBuildsEnabled,用 Timeline 分析下它的渲染耗时,可以通过 Frame events chart 中显示的 build 层级非常深



结合第一部分渲染原理我们了解到,每次定时器刷新 text 数字的时候,整个页面 widget 树都会重新 build,但其实只有最底层 Container 中的 Text 内容在改变,没有必要刷新整颗树,所以这里我们的优化方案是 提高 build 效率,降低 Widget tree 遍历的出发点,将 setState 刷新数据尽量下发到底层节点 ,所以将 Text 单独抽取成独立的 Widget,setState 下发到抽取出的 Widget 内部


class _TestDemoState extends State<TestDemo> {...Widget content(){  Widget result = Column(    children: <Widget>[      ...      Container(          ...          child: Center(            child:                CountText()          )),],  );  return result;}}
class CountText extends StatefulWidget {@overrideState<StatefulWidget> createState() { return _CountTextState();}}
class _CountTextState extends State<CountText> {int _count = 0;Timer _timer;...@overrideWidget build(BuildContext context) { return Text( _count.toString(), style: TextStyle(fontSize: 18, fontWeight:FontWeight.bold),);}}
复制代码


修改后的 Timeline 显示如下图:



build 层级明显减少,总耗时也明显降低


接下来分析下 Paint 过程有没有可以优化的部分,我们打开 debugProfilePaintsEnabled 变量分析可以看到 Timeline 显示的 paint 层级




通过 debugPaintLayerBordersEnabled=true;显示 layer 边框可以看到不断变化的 Text 和其他 Widget 都是在同一个 layer 中的,这里我们想到的优化点是 利用 RepaintBoundary 提高 paint 效率,它为经常发生显示变化的内容提供一个新的隔离 layer,新的 layer paint 不会影响到其他 layer


RepaintBoundary(          child: Container(              margin: EdgeInsets.fromLTRB(10,20,10,10),              height: 100,              width: 350,              color: Colors.yellow,              child: Center(                  child: CountText())),),
复制代码


看下优化后的效果




可以看到我们为黄色的 Container 建立了单独的 layer,并且 paint 的层级减少很多。


常见问题总结


  • 提高 build 效率,setState 刷新数据尽量下发到底层节点

  • 提高 paint 效率,RepaintBoundry 创建单独 layer 减少重绘区域


这两个我们之前的例子已经具体分析过


  • 减少 build 中逻辑处理,因为 widget 在页面刷新的过程中随时会通过 build 重建,build 调用频繁,我们应该只处理跟 UI 相关的逻辑

  • 减少 saveLayer(ShaderMask、ColorFilter、Text Overflow)、clipPath 的使用,saveLayer 会在 GPU 中分配一块新的绘图缓冲区,切换绘图目标,这个操作是在 GPU 中非常耗时的,clipPath 会影响每个绘图指令,做相交操作,之外的部分剔除掉,所以这也是个耗时操作

  • 减少 Opacity Widget 使用,尤其是在动画中,因为他会导致 widget 每一帧都会被重建,可以用 AnimatedOpacity 或 FadeInImage 进行代替


以上内容介绍了些 Flutter 常见的性能问题以及我们怎么用工具检测这个问题,在平时开发过程中要留意规避这类问题。

Flutter-DX 案例分析

近期我们做了个 Flutter 端的动态化模板渲染方案 Flutter-DX,它使用集团 DinamicX 的 DSL,通过下发 DSL 模板,在 Flutter 侧实现动态解析渲染。具体介绍可以参考之前的文章:


《如何在 Flutter 上实现高性能的动态模板渲染》


《做一个高一致性、高性能的 Flutter 动态渲染,真的很难么?》


这里不再详细介绍。


尽管进行了一次渲染架构升级,很大程度上提升性能表现,但是通过高可用线上统计,发现在长列表场景下 fps 值没有达到预期值,所以需要进一步分析哪些操作导致的耗时问题。


以搜索页页面结构为例,外部是 GridView 的容器,里面都是一个个 DX 模板组成的宝贝 card,滑动过程中发现流畅度要明显偏低


所以我们做了以下的优化措施


  • 针对 Sliver 滑动的优化,sliver 在滑动过程中,有一个超出屏幕上下 250 像素的一个缓存区



在列表滚动过程中,DX card 不断的被重建和销毁,没有任何缓存机制,我们在其中加了个缓存池,流程如下,避免 element 不断的被销毁和创建,一定程度提高流畅度



  • 通过 Timeline 分析发现 TextPaint 的 layout 耗时显著,进一步对比分析发现,同样的 UI 显示,带换行符的长文本长度 layout 耗时明显偏高,


后来确认带换行符的文本会影响布局效率,具体分析可以查看 issue



这里我们做的优化措施是在判断只有一行文本显示的情况下,截取换行符前的内容作为 text 文本,从而提升 TextPaint layout 效率。


除此之外,还有一些减少布局层级和简化 build 流程,预加载缓存等措施,实现将 FPS 提升 3 个点,达到一定程度的优化效果。

总结

以上内容分析了 flutter 的渲染原理以及遇到卡顿问题可以用哪些工具从哪些方向入手分析,Flutter 虽然一直宣称流畅度是一大亮点,但也存在一定的优化空间,以及需要开发者掌握一定的开发技巧才能达到更丝滑的体验。


本文转载自公众号闲鱼技术(ID:XYtech_Alibaba)。


原文链接


https://mp.weixin.qq.com/s/iXFa9C68gUHr7PL8NHnZUA


2020-04-28 00:304057

评论

发布
暂无评论
发现更多内容

Higress 开源后,我们整理了开发者最关心的 15 个问题

阿里巴巴中间件

阿里云 云原生 Higress

《天翼云安全白皮书》发布!共铸国云安全生态!

天翼云开发者社区

您有一套专属权益已送达,请注意查收

天翼云开发者社区

安全可信| 密评合规!天翼云全栈混合云通过商用密码应用安全性评估!

天翼云开发者社区

2023-01-11:体育馆的人流量。编写一个 SQL 查询以找出每行的人数大于或等于 100 且 id 连续的三行或更多行记录。返回按 visit_date 升序排列 的结果表。 DROP TAB

福大大架构师每日一题

MySQL 福大大

项目制实践如何助力组织进化

feijieppm

项目管理 研发效能 技术管理 文化 & 方法 效能度量

「认知」打工人的自我修养

职场 认知

IoTLink v1.2.1 最新公告

山东云则信息科技

Java 物联网 springboot

RatingBar(星级评分条)

芯动大师

Android Studio 星级评分条 ratingbar

微服务引擎 MSE 升级至 3.0:降低微服务在云原生时代的演进成本

阿里巴巴中间件

阿里云 微服务 云原生

容器服务与达摩院合作 AHPA 获 AAAI 2023 IAAI人工智能创新应用奖

阿里巴巴中间件

阿里云 容器 云原生

问题盘点|使用 Prometheus 监控 Kafka,我们该关注哪些指标

阿里巴巴中间件

kafka 阿里云 云原生 Prometheus

DIMP:Learning Discriminative Model Prediction for Tracking 学习判别模型预测的跟踪

Geek_7ubdnf

神经网络

大规模 Kubernetes 集群故障注入的利器-ChaosBlade

阿里巴巴中间件

阿里云 Kubernetes 云原生 ChaosBlade

效能改进中的度量实践

feijieppm

项目管理 研发效能 技术管理 文化 & 方法 效能度量

2023年了 对Go做一个全网最全的总结

卡二条

Go Go Concurrency Patterns Go web

TapTap算法平台的 Serverless 探索之路

Serverless Devs

GA-RPN:Region Proposal by Guided Anchoring 引导锚点的建议区域网络

Geek_7ubdnf

神经网络

天翼云荣获2022年度“边缘计算先锋企业”“分布式云先锋企业”称号!

天翼云开发者社区

DW-Siam:Deeper and Wider Siamese Networks for Real-Time Visual Tracking 更宽更深的孪生网络

Geek_7ubdnf

神经网络

GAN:生成对抗网络 Generative Adversarial Networks

Geek_7ubdnf

神经网络

混沌演练实践(一)

京东科技开发者

测试 混沌工程 系统 混沌测试 企业号 1 月 PK 榜

大前端—2022明星项目,2023展望

非喵鱼

JavaScript Vue 前端 React 前沿技术

2023年有哪些具备潜力的加密投资标的?

股市老人

阿里巴巴重磅开源云原生网关: Higress

阿里巴巴中间件

阿里云 云原生 Higress

事件总线 + 函数计算构建云上最佳事件驱动架构应用

Serverless Devs

IoTLink 轻量级的物联网综合业务支撑平台

山东云则信息科技

Java 物联网 spring-boot

什么是云渲染?云渲染速度快吗?

Renderbus瑞云渲染农场

云渲染 云渲染是什么 云渲染速度快吗

应用 Serverless 化,让业务开发心无旁骛

阿里巴巴中间件

阿里云 Serverless 云原生

Java高手速成 | 实现人物拼图游戏

TiAmo

Java 拼图

阿里云AIoT 经典基础知识 快问快答——基础知识

阿里云AIoT

Serverless 物联网 API 消息中间件 弹性计算

复杂业务如何保证Flutter的高性能高流畅度?_架构_檀婷婷(三莅)_InfoQ精选文章