Flutter关于高性能,滚动容器的探索
1、什么是flutter
Flutter 是 Google 推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。开发者可以通过 Dart 语言开发 App,一套代码同时运行在 iOS 和 Android平台。 Flutter 提供了丰富的组件、接口,开发者可以很快地为 Flutter 添加 Native 扩展。
1.1 flutter的框架结构
2、flutter历史
2014.10 - Flutter的前身Sky在GitHub上开源
2015.10 - 经过一年的开源,Sky正式改名为Flutter,低调期
2017.5 - Google I/O正式向外界公布了Flutter,这个时候Flutter才正式进去大家的视野
2018.6 - 距5月Google I/O 1个月的时间,Flutter1.0预览版
2018.12 - Flutter1.0发布,它的发布将大家对Flutter的学习和研究推到了一个新的起点
2019.2 - Flutter1.2发布主要增加对web的支持
Flutter的历史最早可以追溯到2015年,来自Chrome团队的开发人员删除了Chrome里较多兼容老的W3C标准的代码,使性能提升了20倍,随后该方案被命名为Sky。2017年,该项目被重新整理,优先解决移动端跨平台问题,Sky项目重新被命名为Flutter。随后在2018年12月,正式发布了Flutter1.0的Release版本。在2019年,Flutter团队宣布支持Flutter Web以及Flutter桌面版,并在后续持续投入优化。
3、flutter技术的适用场景
Flutter作为一种优秀的跨平台解决方案,有利于企业提高研发效能,降低成本。
基于WebView渲染的技术
基于原生渲染的技术
Flutter设计图
近年来跨平台技术不断涌现,一种以Hybrid、小程序为代表的基于WebView渲染的技术,因此此技术的性能取决于WebView的性能,另一种是以ReactNative、Weex为代表的基于原生渲染的技术,如图二所示,因为多了一层脚本语言转原生UI的过程,所以其理论性能也低于原生App。Google公司摒弃了以上两种方案,采用Dart作为上层App开发语言,其中Dart支持AOT编辑模式,视图数据提供给Skia引擎直接渲染为GPU数据。在理论上,Flutter拥有原生App的性能。
咸鱼?
4、Flutter的优势和问题
- Flutter 的优势
和其他跨端框架相比,Flutter 有很多优势,比如可以脱口而出的高性能、高效率等,这里不再赘述。下面重点介绍实践两年以来,感受到的 Flutter 的最大优势。
(1)资源和技术的打通。在接入 Flutter 之初,最看好它的是“一端编写,多端运行,的优势,但是在运行一段时间后,发现即使Fluter 可以达到提效70%~-80%的效果,单纯的资源效率提升也未必是它最大的优点。它最大的优点是实现了团队内资
源和技术的打通。对于一个小型的业务团队而言,一个团队内单技术栈的人才通常屈指可数,如果开发人数不均衡,就会导致业务不均衡,假如因为一系列的变动以及招聘的困难,半年内i0s开发人员有4名,Android 开发人员只有2名。此时只能权衡业务比重,
Android 侧的业务上线的数量就会变少,除非未来 Android 开发人数能够超过 ios开发人数,否则缺失的这些业务可能永远无法上线。
(2)较少的单技术栈开发人数会使技术交流变少,人员成长速度变慢。而 Flutter先天就可以解决这些问题,资源较多的技术栈进行更多的 Flutter 开发,资源较少的技术栈则进行插件和路由的开发。通过这种方式,Flutter 承担了“松紧带”的作用,技术栈间资源的平衡变得非常容易。
2、Flutter的问题
Flutter 里然在飞速发展,但依然存在一些令人头痛的问题。
(1)工程体系。Flutter 现在最成熟的是纯 Flutter 应用,如果成熟的 App 想要接入Flutter,除了在工程体系上需要按照官方 Add Flutter to existing app 的方式接入,还需要接入一款成熟的混合栈框架(如 FlutterBoost )。这个过程还会遇到一些问题,也会花一定的时间。
(2)开发能力。Flutter 在开发能力方面的问题有一些受限于其成熟度,有一些则是其原理和机制带来的。
(3)JSON/序列化和反序列化。因为 Flutter 禁止了Dart 的反射能力,所以 Flutter不存在类似于 FastJson 这样的 JSON 解析库,这也导致 JSON 文件的解析工作需要使用 Dart 代码逐行实现。比如,一个复杂页面需要1100 行的 JSON 序列化和反序列化
代码。
即使可以通过 Android Studio 的插件自动生成 Dart 代码来节省工作量,但如此庞大的JSON 序列化相关的代码不仅会带来效率问题和运行时风险,也会使 App 运行时内存和包体积增大。
(4)质量。和Native 不同,Dart遇到运行时错误不会导致程序崩溃,只会导致当前的方法体不会继续执行。所以,一个Flutter错误造成的影响在不同的地方是截然不同的,小则不会有任何影响,大则有可能导致整个页面无法正常展示。
因为不会崩溃,所以有些问题不一定能及时发现,这也会导致会有相当一部分数量的Flutter错误被遗漏在线上,给业务带来风险。
- Flutter 错误造成的影响在不同的地方是截
5、Flutter的渲染流程
Container(
color: Colors.blue,
child: Row(
children: <Widget>[
Image.asset('image'),
Text('text'),
],
),
);
对应三棵树的结构如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dKJoVjP4-1641471018685)(picture/v2-871d65414702552fd3daf3e9ed468632_1440w.jpg?lastModify=1641369693)]
当需要更新UI的时候,Framework通知Engine,Engine会等到下个Vsync信号到达的时候,会通知Framework进行animate, build,layout,paint,最后生成layer提交给Engine。Engine会把layer进行组合,生成纹理,最后通过Open Gl接口提交数据给GPU, GPU经过处理后在显示器上面显示,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vH5PDH51-1641471018686)(picture/v2-1e5b51ac70061f9c36c5be080968e881_1440w.jpg.png?lastModify=1641369693)]
如果文本或者image内容发生变化,因为Widget是不可改变,需要重新创建一颗新树,build开始,然后对上一帧的element树做遍历,调用他的updateChild,看子节点类型跟之前是不是一样,不一样的话就把子节点扔掉,创造一个新的,一样的话就做内容更新,对renderObject做updateRenderObject操作,updateRenderObject内部实现会判断现在的节点跟上一帧是不是有改动,有改动才会别标记dirty,重新layout、paint,再生成新的layer交给GPU,流程如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WxDiFn8G-1641471018686)(picture/v2-f76f7b02b7e2818a6ec32f1399f8dde3_1440w.jpg.png?lastModify=1641369693)]
6、 flutter的运行机制
flutter技术主要是由C++实现的FlutterEngine和Dart实现的Framework组成。FlutterEngine负责线程管理、Dart VM状态管理和Dart代码加载等工作。而Dart代码所实现的Framework则是业务接触到的主要API,诸如Widget等概念。
在计算机系统中,图像的显示需要CPU、GPU和显示器一起配合完成:CPU负责图像数据计算,GPU负责图像数据渲染,而显示器则负责最终图像显示。
CPU把计算好的、需要显示的内容交给GPU,由GPU完成渲染后放入帧缓冲区,随后视频控制器根据垂直同步信号(VSync)以每秒60次的速度,从帧缓冲区读取帧数据交由显示器完成图像显示。
操作系统在呈现图像时遵循了这种机制,而Flutter作为跨平台开发框架也采用了这种底层方案。下面有一张更为详尽的示意图来解释Flutter的绘制原理。
可以看到,Flutter关注如何尽可能快地在两个硬件时钟的VSync信号之间计算并合成视图数据,然后通过Skia交给GPU渲染:UI线程使用Dart来构建视图结构数据,这些数据会在GPU线程进行图层合成,随后交给Skia引擎加工成GPU数据,而这些数据会通过OpenGL最终提供给GPU渲染。
七、 Flutter的相关实践(对于高性能,滚动容器的探索)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SbnT8edF-1641471018687)(picture/024ee9f6736243a5929a52e3191b5f9c~tplv-k3u1fbpfcp-watermark.image.gif)]
首先分析卡顿的原因:
我们知道,对于滑动列表的这个过程,其实是由一个个的画面组成,术语称为帧。对于大部分人而言,当每秒的画面达到60,也就是俗称60FPS的时候,整个过程就是流畅的。而不及60FPS的时候,就会产生卡顿的感觉。
一秒 60 帧,也就意味着平均两帧之间的间隔为 16.7ms。如果超过 16.7ms,在观感上就会出现卡顿。通过系统提供的 DevToools 工具可以查看到。
系统为了绘制一帧需要经历哪些阶段?
为什么一帧的耗时会超过16.7ms?为了搞清楚这个问题我们需要知道,Flutter为了绘制一帧会做些什么?
其实我们只需要在任意Flutter工程中,搜索drawFrame() 便可以得到答案。一共有10步骤,其中,与开发者关系比较密切的有下面几步
发生卡顿原因
结合DevTools的分析图,我们可以看出。在上面130ms的构建的主要耗时集中在Layout中调用的build方法
Flutter中ListView采用懒加载机制。对于ListView里面的每一个item,并不会在build阶段全部进行构建。而是在layout阶段,根据屏幕当前的尺寸以及缓存区的范围,动态的构建每一个item。所以引起卡顿的原因非常明显主要由于,在某一帧内,ListView构建多个复杂的item。例如分析图中,在Layout阶段同时build了多个item,一个item的构建耗时已经接近10ms,同时构建自然超过了16ms。
在上文已经介绍过在 Flutter 中,Widget/Element/Render 三棵树的概念。而分帧渲染的原理,其实就是在 Tree 上分层,将一些复杂的节点及其子节点,用一些空 Widget 占位,而原本应该被渲染的节点,放在下一帧去渲染,从而避免出现太复杂的 UI,使得一帧的渲染超过 16.6ms,导致卡顿。(推荐keframe,Flutter 中利用分帧渲染优化流程度的开源库)
哪些场景下容易出现卡顿
- 1、首次进入,列表构建时
当我们打开一个ListView构建的页面时,由于这时ListView中没有任何一个item,所以会进行多次的构建,上面例子的130ms就是如此。
-
2、快速滑动,一帧内构建多个item
当我们在快速滑动的过程中,因为滑动范围比较大,同样可能引起多个item的构建。
-
3、setState进行加载更多
第三个场景是在一些分页列表上,我们往往在数据请求完成后进行setState()更新列表,最终会调用到ListView对应Element的performRebuild()中
其中的_childElements是缓存的item节点(即当前屏幕上以及缓存区的所有item),这里会对每一个item进行update。同时,由于有了更多子节点(item数量增加),所以还会去构建新的item,同样容易引起卡顿。
页面切换流畅度提升
在打开一个页面或者 Tab 切换时,系统会渲染整个页面并结合动画完成页面切换。对于复杂页面,同样会出现卡顿掉帧。
借助分帧组件,将页面的构建逐帧拆解,通过 DevTools 中的性能工具查看。切换过程的峰值变低,整体切换过程更加流畅。
原理
假如现在页面由 A、B、C、D 四部分组成,每部分耗时 10ms,在页面时构建为 40ms。使用分帧组件 FrameSeparateWidget
嵌套每一个部分。页面构建时会在第一帧渲染简单的占位,在后续四帧内分别渲染 A、B、C、D。
假设,我们屏幕能显示4个item,每个item构建耗时是10ms。在现有的 ListView布局过程中,会在第一帧的时候,同时构建这四个item,总共40ms。
采用分帧之后,在页面的第一帧我们先通过构建简单的占位item,占位的item可以是个简单的Container。由于其构建基本不耗时,在第一帧的时候构建四个Container不会导致卡顿。之后将实际的四个item,分别延迟到后面四帧进行渲染。这样对于每个16.7ms而言,都没有发生超时渲染,整个流程不会发生卡顿。
对于列表,在每一个 item 中嵌套 FrameSeparateWidget
,并将 ListView
嵌套在 SizeCacheWidget
内即可
构造函数说明
FrameSeparateWidget :分帧组件,将嵌套的 widget 单独一帧渲染。
SizeCacheWidget:缓存子节点中,分帧组件嵌套的实际 widget 的尺寸信息。
下面是几种场景说明:
1、列表中实际 item 尺寸已知的情况
实际 item 高度已知的情况下(每个 item 高度为 60),将占位设置与实际 item 高度一致即可,查看 example 中分帧优化1
FrameSeparateWidget(
index: i,
placeHolder: Container(
color: i % 2 == 0 ? Colors.red : Colors.blue,
height: 60,// 与实际 item 高度保持一致
),
child: CellWidget(
color: i % 2 == 0 ? Colors.red : Colors.blue,
index: i,
),
)
2、列表中实际 item 高度未知的情况
现实场景中,列表往往是根据数据下发展示,无法一开始预知 item 的尺寸。
例如,example 中 分帧优化 2, placeHolder
(高度40)与实际 item (高度60)尺寸不一致, 由于每一个 item 分在不同帧完成渲染,因此会出现列表「抖动」的情况。
这时可以给 placeholder 设置一个近似的高度。并且在将 ListView 嵌套在 SizeCacheWidget 中。对于已渲染过的 widget 会强制设置 palceHolder
的尺寸,同时将 cacheExtent
调大。这样在来回滑动过程中,已经渲染过的 item 将不会出现跳动情况。
例如,example 中「分帧优化 3」。
SizeCacheWidget(
child: ListView.builder(
cacheExtent: 500,
itemCount: childCount,
itemBuilder: (c, i) => FrameSeparateWidget(
index: i,
placeHolder: Container(
color: i % 2 == 0 ? Colors.red : Colors.blue,
height: 40,
),
child: CellWidget(
color: i % 2 == 0 ? Colors.red : Colors.blue,
index: i,
),
),
),
3、预估一屏 item 的数量
如果能粗略估计一屏能展示的实际 item 的最大数量,例如 10。将 SizeCacheWidget 的 estimateCount
属性设置为 10*2。快速滚动场景构建响应更快,并且内存更稳定。例如,example 中的「分帧优化4」。
SizeCacheWidget(
estimateCount: 20,
child: ListView.builder(
分帧的成本
当然分帧方案也非十全十美,在我看来主要有两点成本:
首先,额外的构建开销:整个构建过程的构建消耗由「n * widget消耗 」变成了「n *( widget + 占位)消耗 + 系统调度 n 帧消耗」。
可以看出,额外的开销主要由占位的复杂度决定。如果占位只是简单的 Container,测试后发现整体构建耗时大概提升在 15% 左右。
这种额外开销对于当下的移动设备而言,成本几乎可以不计。
其次,组件会将 item 分帧渲染,页面在视觉上出现占位变成实际 widget 的过程。
但其实由于列表存在缓存区域(建议将缓存区调大),在高端机或正常滑动情况下用户并无感知。而在中低端设备上快速滑动能感觉到切换的过程,但比严重顿挫要好。
2、LoadMore增量更新
上面我们提到了,item的构建是由ListView的layout驱动,所以如果是增量更新的情况,我们只要修改itemCount之后,标记ListView进行layout即可。闲鱼在文中提到了这个在layout之前需要做Widget缓存的更新,但是实际上在1.22之后,因为这个缓存几乎没有任何优化作用,官方已经去掉了这个Widget缓存,所以这个过程变得更加简单。
3、Element的复用
但是对于Flutter而言,即使item的类型相同,对于不同数据的item而言,并没有一个数据绑定Widget的方法。所以仅仅只能做建立一个缓存池来保存element,创建的时候优先从缓存获取。但这样问题就来了,其实官方本来就有一个cacheExtent缓存区的设计,缓存在cacheExtent内的的Element。个人认为没多大必要额外在做一个缓存。最简单的,将cacheExtent设置大一点就行。
四、非列表的卡顿解决
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YWZqOsWu-1641471018690)(picture/image-20220106111719964.png)]
复杂的页面肯定由复杂的元素组成组成,这里我们column下放了多个row,每个row中放入多个复杂的Widget。这样的例子中,我们可以对每一个row模块嵌套我们的分帧Widget,让每个row进行分帧渲染。
Flutter 分帧上屏源码浅析 https://www.jianshu.com/p/7f1470731ee2
引用:
https://juejin.cn/post/6931920850925191176
https://juejin.cn/post/6940134891606507534