写点什么

复杂业务如何保证 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:303629

评论

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

借力函数计算 FC,HEROZ 打造专业级 AI 日本将棋服务

阿里巴巴云原生

阿里云 云原生 函数计算

华为云助力制造业解决数字化转型困局:高效上云,加速创新

科技怪授

前端线下面授培训机构该怎么选择

小谷哥

前端技术培训学习哪个机构靠谱?

小谷哥

3DCAT实时云渲染助力数字孪生检修车间建设,为智能制造赋能!

3DCAT实时渲染

数字孪生 实时渲染 3D实时云渲染

软件测试/测试开发丨该如何测客户端专项测试?

测试人

软件测试 自动化测试 测试开发 专项测试

时序数据库能做什么|用 GreptimeDB 进行程序员键盘行为分析,最高频按键竟然是它

Greptime 格睿科技

云原生 时序数据库 数据库·

多库多表场景下使用 Amazon EMR CDC 实时入湖最佳实践

亚马逊云科技 (Amazon Web Services)

Java

Excelize 发布 2.7.1 版本,Go 语言 Excel 文档基础库

xuri

开源 编程 Excel Go 语言 Excelize

阿里正式加入ChatGPT战局,“通义千问”上线后表现如何?

引迈信息

AI 阿里 低代码 语言模型 ChatGPT

华为云ERP上云解决方案助力企业迈向云端,提升业务效率与安全性

i生活i科技

如何使用文件传输协议ftp,教你使用文件传输协议命令行

镭速

码头风云——5G降临

白洞计划

5G 智慧码头

“PMC零距离” 赖晖:在 IoTDB 我实现了参与贡献共识协议的兴趣方向!

Apache IoTDB

IoTDB Apache IoTDB

ChatGPT-5到底有多强?Battle!咱貌似也不输呀!

加入高科技仿生人

人工智能 AI 低代码 ChatGPT GPT-4

技术实力与社区贡献的双重认可!乔嘉林成为 Apache 软件基金会 Member

Apache IoTDB

IoTDB Apache IoTDB

IoTDB Raft协议核心研发宋子阳成为Ratis Committer:分布式IoTDB深度并创新运用Raft协议保障高可用

Apache IoTDB

IoTDB Apache IoTDB

新晋 Committer 也有 “产学研联动”?速来围观不同视角共建 IoTDB 社区的故事!

Apache IoTDB

IoTDB Apache IoTDB

StarRocks 3.0 新特性介绍

StarRocks

c++ 数据湖 #java 数据库· 大数据‘’

如何解决制造企业数字化转型中的数据散乱和管理难题,实现顺利转型?

i生活i科技

翻过三座大山:MatrixOne从 NewSQL 到 HTAP 分布式架构演进

MatrixOrigin

分布式数据库 MatrixOrigin MatrixOne 架构升级

历史性的时刻!华为云跨端、跨框架开源组件库项目 OpenTiny 正式升级 TypeScript,10 万行代码重获新生!

英勇无比的消炎药

开源 前端 UI组件库

BUFF NETWORK:去中心化衍生品交易的未来

股市老人

打卡智能中国(三):一位水厂文员的多重身份

脑极体

云计算

华为云助力制造业高效实现数字化转型

科技怪授

什么是安全沙箱技术?如何保护用户隐私和系统安全?

FinFish

前端容器 小程序容器 安全沙箱 小程序安全沙箱

Go 语言读取文件的几种方式

宇宙之一粟

Go 语言

趣谈之什么是 API 货币化?

API7.ai 技术团队

api 网关 APISIX api 货币化

架构师应该具备的特质

agnostic

架构师

MobTech MobPush|推送的下发逻辑是什么样的

MobTech袤博科技

华为云推出场景化解决方案帮助制造企业加速数字化转型

科技怪授

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