飞天发布时刻:2024年 Forrester 公有云平台Wave™评估报告解读 了解详情
写点什么

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

评论

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

世界的下一个主宰——人工智能

CECBC

人工智能 智能时代

初学源码之——银行案例手写IOC和AOP

Java架构师迁哥

bug 回忆录(一)

志学Python

第 0 次面试

escray

程序员 面试 面经

揭秘App的财富密码,剖析算法工程师价值来源

峰池

人工智能 互联网 推荐算法 互联网公司

实践分享丨物联网操作系统中的任务管理

华为云开发者联盟

华为 数据 物联网 进程

深入理解MySQL中事务隔离级别的实现原理

X先生

MySQL 数据库 后端 事务

区块链会替代大数据吗?

CECBC

区块链 大数据

大学四年我是怎么写操作系统和计算机网络的?掏心掏肺的分享!

小林coding

学习 程序员 计算机网络 操作系统 计算机基础

一个草根的日常杂碎(9月27日)

刘新吾

随笔杂谈 生活记录 社会百态

一个草根的日常杂碎(9月28日)

刘新吾

随笔杂谈 生活记录 社会百态

Binder那么弱怎么面大厂?

博文视点Broadview

Java android 通信 移动开发 Android进阶

奈学开发者社区分享:Java - 设计模式的7个设计原则

古月木易

Java 设计模式

奈学开发者社区分享:Java - 设计模式的7个设计原则

奈学教育

Java 设计模式 设计原则

关于深浅拷贝

西贝

Java 大前端 基础

Electron 快速入门及最新安装教程

程序员学院

Java html 大前端 Electron node,js

数字货币是大势所趋,新冠疫情后必须率先发展DCEP

CECBC

数字货币 银行

关于互联网留存和收益你知道多少—带你走近用户成长体系

滴滴普惠出行

华为全联接2020:环信AI领跑,输出5大行业最佳实践

DT极客

三年筑一“用”:长跑中的智能IP网络

脑极体

写给新人算法工程师

峰池

互联网 新人 推荐算法 算法工程师

一文领略 HTTP 的前世今生

yes

互联网 网络 HTTP 阿帕网

牛皮!应届生面试阿里Java岗,七轮过后定级P6,薪资44.8W

面试 计算机基础 编程开发 架构师技能

中国Prime会员独享巅峰64小时超长跨境网购时间

爱极客侠

公有云厂商哪家强?本月UCloud、百度云、阿里云位居三甲——2020年8月云主机性能评测排名

博睿数据

一文纵览向量检索

华为云开发者联盟

数据 搜索 检索 检查

架构1期第三周作业一

道长

极客大学架构师训练营

for-range造就循环永动机?快来看看go中for-range的那些事!

Gopher指北

后端 for Go 语言

戴尔G系列游戏本助玩家激战英特尔大师挑战赛

E科讯

H5选图预览到上传最佳实践

阿里云金融线TAM SRE专家服务团队

android H5

PPT画成这样,述职答辩还能过吗?

小傅哥

Java 小傅哥 流程图 架构师 PPT

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