写点什么

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

评论

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

Linux之more命令

入门小站

Linux

纯CSS实现随机翻牌效果之steps 妙用

devpoint

CSS css3 6月日更

JAVA 面向对象 (十五)-- 异常

加百利

Java 6月日更

区块链服务网络全球首发 将极大降低政务、金融、能源等领域区块链应用成本

CECBC

JavaScript 学习(十二)--- String对象中的模式匹配方法

空城机

JavaScript 大前端 6月日更

实战!使用Docker在线安装OnlyOffice

一个需求

Docker onlyoffice

数据结构——链表

若尘

数据结构 链表 6月日更

线性表、顺序表和链表,你还分不清?

华为云开发者联盟

数组 链表 指针 线性表 顺序表

AQS知多少

卢卡多多

AQS 6月日更 锁分析

你公司里有不能碰的红线吗?

石云升

法律 职场经验 6月日更

带你认识4种设计模式:代理模式、装饰模式、外观模式和享元模式

华为云开发者联盟

设计模式 外观模式 代理模式 装饰模式 享元模式

架构实战营 - 模块 7- 作业

请弄脏我的身体

架构实战营

[译] D8 优化: Assertions

Antway

6月日更

唐庄酒业的酒怎么样?不输茅台特有面子!

Geek_50a546

项目经理的主要工作有哪些?

万事ONES

项目管理 研发管理 ONES

架构实战营 - 群讨论汇总(2021)

华仔

#架构实战营

带你掌握C++中三种类成员初始化方式

华为云开发者联盟

c++ 初始化 类成员初始化 声明时初始化 初始化列表

项目管理复杂多变,如何成为一个好的项目经理?

万事ONES

项目管理 研发管理 ONES 开发管理

Apache Dubbo 3.0.0 正式发布 - 全面拥抱云原生

阿里巴巴中间件

云计算 阿里云 开源 云原生 中间件

Redis - Cluster - 分布式存储

旺仔大菜包

redis

Rust从0到1-函数式编程-闭包

rust 函数式编程 闭包 functional closures

手写Spring,定义标记类型Aware接口,实现感知容器对象

小傅哥

Java spring 小傅哥 aware

做好项目管理,项目经理需要具备哪些优秀品质?

万事ONES

项目管理 研发管理 IT ONES 项目经理

数字化的三大本质

CECBC

带老弟做个实时排行榜

程序员鱼皮

Java c++ 大前端 后端 项目实战

在线正则表达式测试,正则替换工具

入门小站

工具

精致女孩的护牙小心机,藏在这瓶冰泉漱口水里

Geek_50a546

除了数据恢复,EasyRecovery还有这样的功能!

淋雨

文件恢复 Easyrecovery破解 硬盘数据恢复

唐庄酒业的酒怎么样?好的酱香酒就应该和它一样

Geek_50a546

技术实践:教你用Python搭建gRPC服务

华为云开发者联盟

Python gRPC 语言 移动应用开发 RPC框架

☕️【Java技术之旅】【ConcurrentHashMap】深入浅出核心源码分析(JDK1.7版本)

洛神灬殇

Java 源码分析 ConcurrentHashMap 6月日更

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