开始之前,我们需要了解一下 flutter 的四种项目结构
application:纯 flutter 应用
plugin:基于原生的 flutter 插件
package:纯 dart 插件
module:原生上集成 flutter 模块
目前我们项目中使用的就是application结构,包含了各种plugin和package插件。如果采用主原生,就是使用module(原生为主、flutter为辅)。
渲染原理
对于原生 Android 而言,是原生代码经过 skia 最后到 GPU 完成渲染绘制,Android 原生系统本身自带了 skia。
对于 Flutter 而言,Dart 代码里的控件经过 skia 最后到 GPU 完成渲染绘制,这里在 Andriod 上使用的系统的 skia ,而在 iOS 上使用的是打包到项目里的 skia。
首先看下用到的线程:
UIThread
UIThread 是 Platform 创建的子线程,DartVM Root Isolate 所有的 dart 代码都运行在该线程。 阻塞 UIThread 会直接导致 Flutter 应用 卡顿 掉帧。
RasterThread
RasterThread 它是运行在 CPU 用于处理数据提交给 GPU,它的作用是光栅化。
C++ Engine 中的光栅化和合成过程运行在该线程。
整个流程会经过以下几个过程:
C++ Engine 触发 Platform 注册 VSync 垂直信号回调,通过 Platform -> C++ Engine -> Dart Framework 触发整个绘制流程
Dart Framework 构建出四棵树,Widget Tree、Element Tree、RenderObject Tree、Layer Tree,布局、记录绘制区域及绘制指令信息生成 flutter::LayerTree,并保存在 Scene 对象用以光栅化,这个过程运行在 UIThread
通过 Flutter 自建引擎 Skia 进行光栅化和合成操作, 将 flutter::LayerTree 转换为 GPU 指令,并发送给 GPU 完成光栅化合成上屏显示操作,这个过程执行在 RasterThread
整个调度过程是生产者消费者模型。
UIThread 负责生产 flutter::Layer Tree,RasterThread 负责消费 flutter::Layer Tree。
这种调度机制可以确保 RasterThread 不至于过载(2 个任务),同时也可以避免 UIThread 不必要的资源消耗。
所以不论在 UIThread 还是在 RasterThread 耗时太久,都可能会导致 Flutter 应用卡顿,因为会导致延迟接受 VSync 信号,导致掉帧。
flutter(主)+ 原生
当我们在 Flutter 中使用UiKitView将一个原生视图嵌入到 Flutter 中时,UiKitView实际上是将该原生视图添加到了 Flutter 引擎的视图树中,并且提供了一个通信通道,让 Flutter 引擎可以和该原生视图进行交互。
原生(主)+ flutter
FlutterViewController的内部原理是通过创建和管理一个FlutterEngine来实现的,它提供了一个简单的接口,可以将Flutter视图无缝地集成到iOS原生应用中
默认情况下,原生的视图渲染和 Flutter Widget 的渲染是互相独立的,不会出现冲突。
测试下flutter版本为3.0.5
iOS: 原生启动内存大小21.3M,flutter启动内存大小41.5M (release模式,以下测试数据也在该模式)
安卓:原生启动内存大小52M,flutter启动内存大小77 M (profile模式,以下测试数据也在该模式)
包体积对比:
flutter包大小会比 原生的多出约 6M 左右,其中核心引擎大约 3.2MB,框架+应用程序代码大约是 1.25MB,而 assets 文件里还约有 2.1MB 的 ICU 数据等,单纯从安装包上来说,原生是要优于 flutter 。
测试数据
分析:flutter主要比原生多出 libflutter.so动态库,启动需要链接此库,初始化字体等操作。
分析:flutter 在 CPU 和内存的资源占用上会比原生方式多一些,所以单纯的从性能上来说, 原生是肯定要优于 flutter 的,但是从用户体验上来说,两者的滑动同样顺畅无比,几乎感觉不到差别。
分析:动画效果上原生更优,内存占比都差不多。
分析:svga内存占比过大,主要是有几个svga游戏结果的分辨率比较大,svga优化只能pag或者vap上进行代替和生成的时候尽量分辨率和帧率上进行控制
分析:拉流渲染上,flutter也是原生渲染,然后映射到flutter上插入树中,所以相差不大。
Flutter 图片缓存设计的一些问题
应该说 Flutter 的图片缓存设计还是比较契合 Native UI 的使用场景的,但是对于一些设计比较糟糕的 UI,或者是自动生成的类 Web 的长页面,这样的设计可能会造成一些灾难性的后果。
1. Flutter 解码的时机非常靠前,如果一次性加入大量的 Image Widget 对象,会马上产生相应数量的加载和解码任务,这可能造成系 统较为严重的阻塞,并且部分 Image Widget 实际上可能距离可见区域较远,解码后产生的纹理暂时不会被使用,这造成了内存浪 费;
2. ImageCache 实际上是没有真正封顶的(Live Pool 是无上限的),如果当前的 Widget 树同时包含了大量的 Image Widget,内存 峰值可能会非常夸张,很容易造成 OOM;
3. ImageCache 是在 Framework 层的实现而不是 Engine 层,它的实例由 Widget 层产生,通过 PaintingBinding.instance.imageCache 访问,这意味着每个 FlutterView,每个 Root Isolate 都有一个不同的 ImageCache,如果是混合应用,同时展现多个 FlutterView,不但 ImageCache 的 Live Pool 没法控制,Cache Pool 也会处于叠加的状态,导致内存的峰值会更难以控制;
总结:
通过以上的分析,原生在内存、CPU 资源占用方面要低于 flutter,并且安装包的体积也要小于 flutter,所以,不考虑其他因素,单纯从性能角度来说, 原生肯定是要优于 flutter 的。但 flutter 也有它的优点,比如跨平台的开发、毫秒级的热重载等等,另外跨端开发也逐渐的流行起来,所以,我们在学好原生的基础上,对跨端开发也要抱有积极的心态。