前言
有幸负责的模块使用Flutter编写,在三个月的开发过程中,在原有Demo自学基础上又学到了很多,谨以此篇文章做一个Flutter阶段性的学习和总结,以便于往后的学习过程中温故而知新,那么我们正篇开始。
前世今生
新事物的诞生往往是有一定原因存在的,移动端在这条路上有几个阶段,从Android Native 到 WebView 阶段,为了获得不发版本就可以获得实时动态化的效果,双端使用JSBridge实现了与原生Native底层能力的对接。
Native容器阶段,主要有React Native为代表,虽然RN依赖于原生渲染,性能好与H5,但是还存在一些问题,比如:JavaScript和原生通信,部分场景存在通讯瓶颈,容易导致卡顿、不同移动平台,空间需要单独维护,原生版本更新后,社区控件更新较慢等问题。
Flutter阶段,Flutter抛弃了原生系统控件和 Webview,使用自研高性能渲染引擎来绘制Widget,预先(AOT)编译,运行时直接执行Native(arm)代码,Dart代码执行(在UI TaskRunner),图片下载(IO TaskRunner),真正的渲染(GPU TaskRunner),同平台的通信等(Platform TaskRunner即Native概念下的主线程)是互相隔离的。针对布局等的优化:布局计算时单次树走动即可完成;Relayout Boundary机制:如果Child 的size是固定的,那么不会因为Child的Relayout导致Parent ReLayout等布局优化,都让Flutter脱颖而出。
所以,App引入Flutter,可以有效提高开发效率,避免双端不一致的现象,缩小开发成本。但是Flutter毕竟不是原生,在代码写法上有很多需要注意的地方,文章第三章会主要总结本人在开发和工作中遇到的问题,相比于H5,flutter在动态化方面,支持的并不好。
1 基础知识
1.1 运行模式
Debug:Debug模式可以在真机和模拟器上同时运行:会进入所有断点,包括debugging信息、debugger aids(比如observatory)和服务扩展。优化了快速develop/run循环,但是没有优化执行速度、二进制大小和部署。命令flutter run就是以这种模式运行的。
Release : Release模式只能在真机上运行,不能在模拟器上运行:会关闭所有断言和debugging信息,关闭所有debugger工具。优化了快速启动、快速执行和减小包体积。禁用所有的debugging aids和服务扩展。这个模式是为了部署给最终的用户使用。
Profile:Profile模式只能在真机上运行,不能在模拟器上运行:和Release模式类似,除了启用了服务扩展和tracing,以及一些为了最低限度支持tracing运行的东西(比如可以使用DevTools)。flutter run --profile 可进入该模式。
Test :headless test模式只能在桌面上运行:和Debug模式不同的是headless的而且你能在桌面运行。命令flutter test就是以这种模式运行的。
1.2 架构设计
Framework : 使用dart实现,包括Material Design风格的Widget,Cupertino(针对iOS)风格的Widgets,文本/图片/按钮等基础Widgets,渲染,动画,手势等。
Engine : C++实现,主要包括:Skia,Dart和Text。 Skia是开源的二维图形库,提供了适用于多种软硬件平台的通用API。
Embedder : 嵌入层,即把Flutter嵌入到各个平台上去,这里做的主要工作包括渲染Surface设置,线程设置,以及插件等。 从这里可以看出,Flutter的平台相关层很低,平台(如iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。
1.3 渲染流程
Widget:Widget树实际上是一个配置树,而真正的UI渲染树是由Element构成;不过,由于Element是通过Widget生成,所以它们之间有对应关系,我们可以宽泛地认为Widget树就是指UI控件树或UI渲染树。
Element : 一个Widget对象可以对应多个Element对象。这很好理解,根据同一份配置(Widget),可以创建多个实例(Element)。
从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。
举个栗子?
以下例子取自 闲鱼技术博客 闲鱼Flutter复杂业务优化
Container(
color: Colors.blue,
child: Row(
children: [
Image.asset('image'),
Text('text'),
],
),
);
依据上图 来说就是 UI 刷新的时候,Framework 通知 Engine,Engine 会等到下个 Vsync 信号到达的时候,会通知 Framework 进行 animate, build,layout,paint,最后生成 layer 提交给 Engine。Engine 会把 layer 进行组合,生成纹理,最后通过 Open Gl 接口提交数据给 GPU, GPU 经过处理后在显示器上面显示
在基础能力和业务开发完成后,性能优化和代码检查也是非常重要的,所以后边的文章主要围绕 性能检测和优化 等细节的总结。
3 性能检测工具
结合 Flutter 性能分析 链接 和Flutter 性能视图 等官方文档,整合调试工具说明,并添加自我理解
3.1 Performance Overlay
开启方式比较多,通过 DevTools TimeLines 或者 run Flutter Inspector 都可以进入
点击show CPU GPU 图后
● 竖轴表示耗时,沿竖轴的黑线是时间线 (间隔单位为 16ms)
● 横轴则表示帧,垂直的绿色条代表的是当前帧 (如果为红色则代表处理耗时点)
● 上图显示GPU线程消耗的时间 (帧为红色:Widget太多 场景复杂 渲染耗时)
● 下图显示UI线程消耗的时间 (帧为红色:Dart代码有问题 查看build中是否做太多耗时操作)
也可以在代码中开启 PerformanceOverlay 控件
@override
Widget build(BuildContext context) {
Widget app = MaterialApp(
// true 展示PerformanceOverlay
showPerformanceOverlay: showPerformanceOverlay,
title: 'appname',
theme: ThemeData(
appBarTheme: AppBarTheme(brightness: Brightness.light),
textTheme: textTheme
),
home: _buildHome(context),
builder: (_, widget) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: widget
);
},
);
return WillPopScope(
child: app,
onWillPop: dealWillPop,
);
}
showPerformanceOverlay 为MaterialApp自带属性,如果没有使用MaterialApp,可以在代码中通过PerformanceOverlay.allEnabled(checkerboardOffscreenLayers: true); 开启
3.2 DevTools TimeLine
使用 DevTools 可以在Performance Overlay大粒度卡顿范围内深度挖掘
由于Observatory和DevTools功能类似,且正逐渐被DevTools替代,这里主要罗列DevTools的使用方式。
● 顶部部分列表类似于PerformanceOverlay ,深色代表GPU耗时,浅红色代表UI层耗时(图层过多)
● 中间部分为帧事件图表,分位 UI 和 GPU两部分组成
● 底部部分为CPU图表,主要有三种不同呈现方式来展示CPU调用堆栈信息
我们也可以通过添加一些系统api ,更方便我们定位事件图表具体执行位置
● debugProfileBuildsEnabled - 向 Timeline 事件中添加 build 信息
● debugProfilePaintsEnabled - 向 timeline 事件中添加 paint 信息
● debugPrintRebuildDirtyWidgets - 记录每帧重建的 widget
和Android Profiler 类似,帧事件横轴代表事件顺序,纵轴 代表方法调用堆栈,我们可以通过事件宽度来判断那些事件是比较耗时的。
3.3 Widget rebuild stats
Android Studio 中 View > Tool Windows > Flutter Performance 打开性能工具窗口,在 Widget rebuild stats 中勾选 Track widget rebuilds 来查看 widget 的重建信息。重建信息包括 Widget 名字、源码位置、上一帧中重建次数、当前页面中重建次数。此外,Widget 名字前面还会显示一个小图标。
● 黄色旋转圆圈 - 重建次数过多
● 灰色圆圈 - 未重建
● 灰色旋转圆圈 其他情况
4 性能优化
上一章,对于检测工具大概的总结了使用方式和看法,下边我们就根据工具,来定位界面的问题,并总结出我的优化方式。
4.1 列表优化
在商品2.0中列表占比是比较多得,首页列表的交互逻辑更为复杂,如何让用户用起来如丝般顺滑,需要进行一些性能的打磨。
问题:
界面滑动的时候 会有轻微掉帧现象,检测 fps长期飘红(小于 60fps)
(图一 Flutter Performance 截图)
(图二 DevTools Fps ui GPU 截图)
方案:
更换PowerScrollView 后列表 滑动FPS和部分DevTools截图
PowerScrollView :概念链接
PowerScrollView 优化点:
- 针对PowerDataManager 数据可以做到增量更新。
- 添加缓存数组,监控_childElements方法来对element 进行缓存,当滚动超出 viewport 的显示以及预加载范围或者数据源发生变化,会通过调用 collectGarbage 方法回收不需要的 elements)
- cell 层面引入了 placeholder 的机制,快速滑动场景优化build工作量
- 更多样的瀑布流 、列表样式可供选择等等优化
引入后效果,
(图一 Flutter performance 截图)
图中红色部分主要处于手指第一次按下,并且执行了顶部商品工具栏动画的原因
(图二 DevTools FPS UI GPU 截图)
上图可以看到 gpu占比较多,列表滑动起始部分还是有卡顿部分,主要是由于首页商品工具栏过度动画 和 首页复杂的UI展示引起的,后续可以通过
4.2 优化 ClipPath 和 ClipRPath
在刚开始调试初期,使用 Timeline 查看渲染线程性能消耗,可以发现有多个 ClipRectLayer 和 ClipRRectLayer过程,对比商品首页界面后,猜测是在Flutter壳工程中使用自带Image展示图片的时候,对于大图没有进行裁剪显示,部分圆角Widget也需要优化,同时修复 radius 为0也会设置 ClipRRect 的问题。
4.3 常用优化方法
- 尽量将setState放在叶子节点,好处是build时影响范围极小,局部刷新
- 使用ListView.builder()而不是直接使用ListView()来构建列表 (官方推荐)
- 对于频繁更新的控件,使用RepaintBoundary隔离,让其拥有一个独立的paint区域
- 使用const来修饰永远不需要变更的控件,如果宽高固定,推荐固定宽高,避免重复计算
- 按需使用StateLessWidget,并非全部用StateFulWidget
- 使用Visibility控件,避免布局树频繁切换
- 针对于使用Fish-redux,state 对象中的视图数据真正发生变化的时候,新建 state 对象
- 使用图片替换半透明效果,减少saveLayer 或者clipPath 的使用。
最后
不知不觉已经写到了最后,对于Flutter的学习和使用,一直都在路上,文章中不免有些纰漏,还望大佬们海涵并指出,后续会继续深耕Flutter 动态化方案和Flutter性能优化,继续努力学习,继续满血冲冲冲!