美团外卖 Flutter 性能优化与实践
如果无法正常显示,请先停止浏览器的去广告插件。
1. Flutter 性能优化与实践
吴晓⻰
美团技术专家
2.
3. 吴晓⻰
• 2019 年加入美团外卖技术部商家研发组
• 曾就职于百度、金山
• 现为 Flutter 动态化核心成员之一
4. 1 Flutter 性能指标采集与监控体系
2 Flutter 在客户端上关键指标优化与实践
5. Flutter 性能指标采集与监控体系
① 框架层异常
② 引擎层和嵌入层崩溃
③ FPS 最优采集口径
④ Flutter 秒开率概述及算法原理
⑤ 大盘指标
6. ① 框架层异常
网络环境
运行环境
runZoned
渲染逻辑
JSON 解析
FlutterError.onError
手势识别
7. ① 框架层异常
多个 Zone 间通过 parent 属性形成链式结构
runZoned
怎么捕获异常?
Zone 内部用 try-catch 包裹执行体,从而
将执行体内的所有异常捕获,然后将捕
获到的异常及调用栈传递给 onError 回调
8. ① 框架层异常
以RenderObjectToWidgetElement的_rebuild方法为例
与预期值不同时将抛出异常
FlutterError
抛出的异常怎么收集?
9. ① 框架层异常
业务代码产生
Framework 层抛出
重写 onError
通过 Zone 获取
10. ② 引擎层和嵌入层崩溃
复用收集方案
自动符号化
与引入的其他三方库并无二致,通过原 Native 的崩溃收
集方案采集上报
符号表和带符号的 libflutter.so 在自己手里,MTFlutter 团队
和 Crash 平台团队协作,实现 Flutter 崩溃日志自动符号化
11. ③ FPS最优采集口径
Native
Flutter
Android
Choreographer.FrameCallBack
iOS
CADisplayLink
12. ③ FPS最优采集口径
采集方案
原生方案不能用 分析工具有限制
渲染引擎Skia直接和绘图相关的底
层接又对接,绕过原生渲染引擎 官方提供的调试分析工具只能在
Debug 和 Profile 模式下开启
13. ③ FPS最优采集口径
Release 模式 FPS 计算方式探索
Engine
Vsync
VsyncWaiter
::FireCallback
Framework
Shell
::OnAnimatorBeginFrame
Window
::BeginFrame
onBeginFrame
Window
onDrawFrame
SchedulerBinding
._handleBeginFrame
._handleDrawFrame
14. ③ FPS最优采集口径
onReportTimings
FrameTiming
提供一个包含FrameTiming的List参数
提供非常细致的时间开销
给window对象注册 onReportTimings,就可以拿到整个构建渲染过程的完整耗时
15. ④ Flutter 秒开率概述及算法原理
秒开率 页面首屏时间 < 1s 的比率
用于衡量终端服务内容呈现速度
页面实现秒开有助于电商增加营收
优化前 | 等待
优化后 | 秒开
16. ④ Flutter 秒开率概述及算法原理
界定达成FMP触底算法 可见元素布局第一次超出屏幕底部的时间
Text、Image、Button 等视图元素
FMP(First Meaning Paint):首次有效绘制,
标记主角元素渲染完成的时间点
全屏⻚面结束示例
17. ④ Flutter 秒开率概述及算法原理
界定达成FMP触底算法 交互前最后一次首屏范围内的视图树变动
Text、Image、Button 等
其他⻚面结束示例
18. ⑤ 大盘指标
FPS大盘
趋
势
秒开率大盘
对
比
FPS值
度量质量、横向对标
19. 小结
• 框架层异常(runZoned、FlutterError.onError)
• 引擎层和嵌入层崩溃(崩溃日志自动符号化)
• Flutter FPS 最优采集口径(FrameTiming)
• Flutter 秒开率概述及算法原理(可视元素的判定、结束点的判定)
• 大盘指标(度量质量、横向对标)
20. Flutter 在客户端上关键指标优化与实践
① 基于动态下发的 Flutter 包大小优化方案
② 性能优化工具
③ 性能优化思路
④ 外卖商家端商品列表性能优化实践
21. libapp.so
libflutter.so
flutter.jar
flutter_assets
310.5KB
Flutter 引擎
Flutter 引擎的 Java 代码编译产物
flutter_assets
3%
170KB
包括图片、字体、LICENSE 等静态资源
Flutter 业务
flutter.jar
6%
1.5MB
libapp.so
29%
Flutter 业务与框架的 Dart 代码编译产物
3.2MB
Flutter 资源
Flutter 引擎的 C++ 代码编译产物
libflutter.so
62%
Flutter Android 产物组成
22. 稳定性
动态性
优化收益
删减法
压缩法
支持粒度
用户体验
动态下发
扩展性
双端复用性
易维护性
23. ① 基于动态下发的 Flutter 包大小优化方案
打包处理
打包 自定义 Gradle Plugin 对 Flutter 产物处理
过滤 build.gradle 中增加 abiFilters 进行过滤,只保留单架构的 so
预处理 将无用的 LICENSE 文件移除,将 assets 中的文件打包 bundle.zip
上传 通过 Dynloader 上传插件将过滤文件从 APK 中移除,上传到 DD 系统托管
24. ① 基于动态下发的 Flutter 包大小优化方案
特殊处理
Flutter 路由拦截
自定义引擎初始化
自定义资源加载
25. ① 基于动态下发的 Flutter 包大小优化方案
自定义引擎初始化
继
承
重
写
start
sSettings==
null
sSettings
FlutterMain.
startInitilization
sSettings=setting
System.loadLibrary(“flutter”)
① 一般在 Application 初始化时初始化
sInitalized=
=false
② 在 Flutter 页面启动时确保初始化的完成
FlutterMain.
sInitialized
ensureInitializetionComplete
FlutterJNI.nativeInit
sInitialized = true
Android 侧 Flutter 引擎初始化流程图
End
26. ① 基于动态下发的 Flutter 包大小优化方案
自定义资源加载
我们无法修改 PlatformAssetBundle 原有的资源加载逻辑,
但是我们可以自定义一个资源加载器对其进行替换:
在 Widget 树的顶层通过 DefaultAssetBundle 注入
27. ① 基于动态下发的 Flutter 包大小优化方案
如何将这些模块合理的加以整合呢?
平台团队实现 FlutterDynamic ,其结构图如右:
28. ① 基于动态下发的 Flutter 包大小优化方案 | Android
打包
阶段
Flutter 产物 预处理
java 代码 移除无用的 assets
多架构的 so flutter_assets zip 打包
flutter_assets
过滤多余架构的 so
Apk hash 计算 DD 系统
资源与 so 的
移除和上传 瘦身apk
当 Flutter 业务的不断迭代增⻓
时,Flutter 产物包也会随之不断
变大,最终导致需下载的产物变
运行
阶段
Flutter 路由拦截器
DynLoader 下载管理
自定义引擎初始化
大,也会对下载成功率带来压力
自定义资源加载器
其中的 DynLoader 是平台迭代工程组为公司提供了一套 so 与 assets 的动态下发框架
字体动态加载
29. ① 基于动态下发的 Flutter 包大小优化方案 | Flutter 动态化(Flap)细粒度的动态下发
彻底的动态化方案 视图逻辑一体化
现有代码一键生成可下载执行文件
趋近零成本迁移 原 Flutter 页面 98% 代码无需修改
经受线上大量验证 外卖商家 90% 页面已动态化,共 150+ 页面
配套丰富
IDE 插件语法提示,开发面板辅助,一键锁包及异常定位到代码行号等
30. ① 基于动态下发的 Flutter 包大小优化方案 | Flap 产物(Bundle)体积瘦身
随着动态化业务发展,动态化产物的体积日渐增大,
体积过大会导致下载成功率降低,下载时长超长,增加带宽成本等问题
缩小 Bundle 体积势在必行
31. ① 基于动态下发的 Flutter 包大小优化方案 | Flap 产物(Bundle)体积瘦身
原有方案:
JSON 文件
压缩成行
加密
转字节流
所有文件压
缩打包
存在的问题: 单文件加密过后整体做压缩时压缩率基本只能维持在100%
结论:
压缩算法本身有提升空间
JSON 内容冗余:字段名太长、重复内容多
32. ① 基于动态下发的 Flutter 包大小优化方案 | Flap 产物(Bundle)体积瘦身
产物生成阶段
改进方案
JSON 写入
文件
生成 .flap
整个 ZIP 进
行加密
产物使用阶段
重新生
成.flap文件
Bundle 解密
转字节流
字节流转成
JSON 文件
组
JSON 文件
加密成 .flap
① 压缩前不对 JSON 进行处理,以便对 JSON 进行极限压缩;
特点
② 对压缩后的文件进行加密,保证传输过程中产物安全;
③ 端上 SDK 将解密后的 JSON 再进行一次加密,写到沙盒中,保证本地产物安全。
33. ① 基于动态下发的 Flutter 包大小优化方案 | Flap 产物(Bundle)瘦身收益
25个 Bundle
瘦身前 瘦身后 瘦身率
mine 131KB 29KB 77.86%
im_home 335KB 73KB 79.21%
… … … …
25个
Bundle 3.6MB 779KB 78.36%
34. ① 基于动态下发的 Flutter 包大小优化方案 | 小结
瘦身效果
瘦身质量
100%
100%
95%
75% 75%
50% 50%
30%
25%
0%
iOS
动态下发方案
99.99%
下载成功率 ⻚面加载成功率
25%
0%
Android
99.99%
Flutter 动态化(Flap)
35. 小Team,木法搞呀?
领导说:
• 包大就大吧
• 有没有现成能用的优化方案
• 把咱们体验也搞一搞
36. 业务性能优化怎么做?
问题页面
发现问题
优化工具
优化方式
解决问题
实际操作
验证结果
37. ② 性能优化工具 | CPU 性能优化工具
帧渲染图表
Timeline view
CPU 分析器
配置环境
38. ② 性能优化工具 | Skia 性能优化工具
1、 捕捉 Timeline
分析 Flutter 应用的 Skia 调用
flutter run --profile —trace-skia
图表部分的背景色覆盖了
优秀的 Flutter 代码在减少每次帧渲染过程的更新数据
前面增加的背景色指令
2、 捕捉 skp
flutter screenshot --type=skia —observatory-uri=uri
捕捉 Skpicture 分析每一条绘图指令
39. ③ 性能优化思路 | 减少构建负荷
Root
StatefulWidget
Root
StatefulWidget
setState
① 降低遍历的出发点
② 控制 build 方法的耗时
优秀的 Flutter 代码在减少每次帧渲染过程的更新数据
StatefulWidget
仅仅只有
这里需要
刷新
setState
StatefulWidget
仅仅只有
这里需要
刷新
40. ③ 性能优化思路 | 提高构建效率
① 对列表和网格列表懒加载
② 使用 const 来修饰永远不需要变更的控件
优秀的 Flutter 代码在减少每次帧渲染过程的更新数据
③ 优先使用 StatelessWidget ,而不是全部用 StatefulWidget
④ 使用 Visibility 控件替换 if/else ,或 return 占位控件,树结构不会发生改变
41. ③ 性能优化思路 | 提高绘制效率
Paint
RenderObject
对于频繁更新的控件,
Layer
RepaintBoundary
使用RepaintBoundary 隔离它,
优秀的
Flutter
代码在减少每次帧渲染过程的更新数据
让它在一个独立的 paint 区域
RepaintBoundary
42. ③ 性能优化思路 | 提高GPU效率
透明度 Opacity
创建带圆⻆的矩形 应用剪切矩形
AnimatedOpacity
FadeInImage
borderRadius属性
优秀的 Flutter 代码在减少每次帧渲染过程的更新数据
AnimatedBuilder
动画
Clipping
依赖于动画的 Widget 的构造方法
中构建 Widget 树
动画中剪裁
Clip.antiAliasWithSaveLayer
预先构建子树
动画开始之前预先剪切图像
没有 Opacity 那么耗时,但仍然很
耗时,所以请谨慎使用
43. 业务逻辑实现复杂
大量接口和交互
动画、吸顶等效果
44. 优化前
常规优化
优化后
⻚面组件的刷新范围过大,
导致单帧构建耗时过⻓
优秀的 Flutter 代码在减少每次帧渲染过程的更新数据
商品列表数据解析完成后
触发的组件 build
当前数据变化无关的页面
组件的 build 记录
45. 优化前
动画优化
优化后
在弹窗完全展示的每一帧都会重新 build 弹窗的内容
优秀的 Flutter 代码在减少每次帧渲染过程的更新数据
只在首次触发了
弹窗内容的 build
46. 优化前
只是更新了单个商品的内容,
但列表的整体都重新 build 了
列表局部刷新
优化后
a. 单个 Item 构建耗时长
优秀的 Flutter 代码在减少每次帧渲染过程的更新数据
达到局部刷新
b. 刷新机制不合理
c. 累加开销大
47. ④ 外卖商家端商品列表性能优化实践
Viewport 的刷新
别
讲
代
码
当 setState 触发 ListView 的 update 时,如果满足条件,即会触发 performRebuild
performRebuild
清空 childWidget 缓存
重新 build child Widget
update child element
删除
修改
新增
所有子元素的 rebuild
列表刷新耗时较高
与预期不符
48. ④ 外卖商家端商品列表性能优化实践
存在问题
优化方案
修改私有变量势必要重新相关的父类实现
细分场景区分、刷新的细粒度控制等引入了新的工作量
区分 loadMore、insert、delete 等场景对缓存 map 做处理
49. ④ 外卖商家端商品列表性能优化实践
updateChild 的参数包含了 Element 类型的 child 和对应的新的 Widget 配置 newWidget,可划分为四种情况:
newWidget 为空
child 为空 newWidget 为空
child 非空 newWidget 非空
child 为空 newWidget 非空
child 非空
Null Remove 新建 更新
直接返回 null 直接调用 通过 inflateWidget 更新 child 树
deactivateChild 方法 创建一个新的 Element 并返回
执行清理工作 并返回 若是不能更新child
且返回null
则通过 inflateWidget
创建一个新的 Element 并返回
50. ④ 外卖商家端商品列表性能优化实践
newWidget 非空,child 非空,则是尽可能更新 child 树,并返回,
若是不能更新 child,则通过 inflateWidget 创建一个新的 Element 并返回。
具体的可细分为三种情况:
继承 SliverChildBuilderDelegate 实现 CacheSliverChildBuilderDelegate;
接受一个 LRU map
用于缓存,并在 Selector
delegate 调用 builder
方法做一层代理;
缓存 Element
创建新的 Element
首先判断是否有对应的缓存 Widget,缓存未命中时创建新 Widget 并保存。
性能较优
如果非同一个 widget,但是
Widget.canUpdate(child.widget,
newWidget) 为 true ,意味着可以通过
调用 update 来用新的 widget 配置更
新 element
性能最佳
若 newWidget 和 child 本身持有的
widget 是同一个widget,则只是调用
updateSlotForChild 更新 Element 对
应的 slot
性能一般
若非前两种情况,则退化为通过
inflateWidget 创建新的 Element 并
返回
51. ④ 外卖商家端商品列表性能优化实践
其他优化
只刷新当前选中
分帧渲染
Tab
重复 build 通过
优秀的 Flutter 代码在减少每次帧渲染过程的更新数据
缓存 widget 解决
Provider 拆分
JSON 懒解析
页面不可见时
滑动时延迟刷新
不刷新
…
52. ④ 外卖商家端商品列表性能优化实践
Android
筛选维度
四个版本完成了第一阶段的优化
优化效果有大幅提升
版本趋势
低端机上流畅度提升更加明显
给用户提供更好的使用体验
收益评估
优化效果
53. 总结
睡着了……刚刚说了啥?
1. “工欲善其事必先利其器”,我们需要先找到适合做优化分析的工具;
2. 其次,优化前期需要“抓大放小”,先解决关键问题,解决那些存在的问题;
当我们真正需要解决线上性能问题的时候,我们需要先准确的采集指标,
然后通过大盘分析,找出其中的问题,除了优化外,我们还要做好防劣化
措施;
3. 优化思想是围绕最核心的关键点展开,我们可以从生活中获得启发;
54. 总结
布局优化
分帧渲染
延迟加载
数据预取
懒解析
按需加载
优秀的 Flutter 代码在减少每次帧渲染过程的更新数据
错峰加载
预构建
并行分解
缓存复用
局部刷新
…
55. 扫他
56.