写点什么

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

2020 年 4 月 28 日

复杂业务如何保证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 年 4 月 28 日 00:301815

评论

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

苏宁云商向江旭:是时候让技术成为新司机了!

TGO鲲鹏会

【JAVA】感受下JDK14的空指针提示

遇见

Java jdk jep

ELF文件格式

韩超

从西游到武侠——确定性与不确定性

伯薇

个人成长 管理 确定性 不确定性

3000w人民币的学费——我的决策反思

孤岛旭日

数据中台 架构 中台 企业中台 企业架构

中台之路,从平台到中台的思考与实践(一)

孤岛旭日

架构 中台 企业中台 企业架构

聊聊分心这件事

Jackey

Doris 一种实时多维分析的解决方案

迹_Jason

大数据

开源这件事儿,越来越“声势浩大”了

赵钰莹

Apache GitHub 阿里巴巴 开源 腾讯

NVidia-Docker2 性能优化

薛磊

Docker gpu nvidia container

Docker Swarm 踩坑

ikook

Docker Docker Swarm 技术 容器 踩坑

ArrayList、LinkedList 你真的了解吗?

Damon

Java

面试官,不要再问我三次握手和四次挥手

猿人谷

面试 TCP 三次握手 四次挥手

特定系统的Linux的构建

韩超

基于RocketMQ实现分布式事务 - 完整示例

清幽之地

Java 分布式事务 RocketMQ 微服务

百度主任架构师谭待:打造非职权技术管理机制

TGO鲲鹏会

自动驾驶复苏在2020

陈思

人工智能 自动驾驶

程序员通过哪些方式来赚钱?

一尘观世界

程序员 外包 自由职业 副业 赚钱

我使用了哪些生产力工具?

ikook

效率工具 软件 Alfred Notion 推荐

Gitlab CI/CD 中的 Cache 机制

Chong

DevOps gitlab cicd

字节跳动的增长密码

池建强

字节跳动 张一鸣

高手和普通人的差距,不看不知道,一看吓一跳

熊斌

学习

服务降级的常见套路

松花皮蛋me

Java

Linux的proc文件系统编程

韩超

NVidia Docker介绍

薛磊

Docker

Kylin 实时流处理技术探秘.笔记

迹_Jason

大数据

人间至味——苦瓜

三只猫

人生 美食 生活

纯技术改造,技术如何驱动需求,我有话说

一叶而不知秋

项目管理 架构 技术

redis数据结构介绍-第一部分 SDS,链表,字典

Nick

redis 源码 数据结构 源码分析 算法

[KubeFlow] MPI-Operator深度解读

薛磊

Docker gpu kubeflow Kubernetes

中台之路,从平台到中台的思考与实践(二)

孤岛旭日

架构 中台 企业中台 企业架构

混合云之争的开端与终途

混合云之争的开端与终途

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