Flutter与其他跨平台方案的区别
ReactNative之类的框架,只是通过JavaScript虚拟机扩展调用系统组件,由Android和iOS系统进行组件的渲染;Flutter则是自己实现了一套组件渲染的逻辑。
Flutter的底层图像渲染引擎是Skia(它同时也是Android官方的图像渲染引擎)。Skia 是一款用 C++ 开发的、性能彪悍的 2D 图像绘制引擎,因为其出色的绘制表现被广泛应用在 Chrome 和 Android 等核心产品上。Skia 在图形转换、文字渲染、位图渲染方面都表现卓越,并提供了开发者友好的 API。Skia 已经是 Android 官方的图像渲染引擎了,因此 Flutter Android SDK 无需内嵌 Skia 引擎就可以获得天然的 Skia 支持;而对于 iOS 平台来说,由于 Skia 是跨平台的,因此它作为 Flutter iOS 渲染引擎被嵌入到 Flutter 的 iOS SDK 中,替代了 iOS 闭源的 Core Graphics/Core Animation/Core Text,这也正是 Flutter iOS SDK 打包的 App 包体积比 Android 要大一些的原因。Skia 保证了同一套代码调用在 Android 和 iOS 平台上的渲染效果是完全一致的。
Flutter整体架构分三层:Framework层、Engine层、Platform层。
Framework层是用Dart实现的,核心内容包括Widget(声明式UI)、Element(diff)、RenderObject(布局+绘制),类似React和Vue的虚拟DOM思想。
Engine层由C++实现,核心组件包括负责画UI的Skia图形引擎,运行Dart代码的Dart Runtime,和 Text / Image / Animation 支持。
Platform层(系统)只做三件事:提供窗口(Surface)、处理输入(触摸、键盘)、提供系统能力(相机、蓝牙等)。

StatefulWidget生命周期

- createState:执行完毕后表示当前组件已经在组件树中
- initState:只会被调用一次,在其中进行初始化操作,如加载网络数据、添加监听器等
- didChangeDependencies:在initState后或其所依赖的
InheritedWidget发生变化时调用 - build:在标脏后的下一帧被调用
- didUpdateWidget:当组件的configuration发生变化时调用此函数
- deactivate:当框架从树中移除此State对象时将会调用此方法,deactivate后还可以重新插入到树中
- dispose:当框架从树中永久移除此State对象时将会调用此方法,在其中释放相关资源
Flutter的缺点
包体积偏大
由于Flutter是通过Skia引擎实现自绘UI,但iOS没有Skia,因此iOS包要额外包含Skia引擎,导致包体积偏大。
与原生通信性能较差、交互复杂
因为与原生通信是走Method Channel,数据序列化有性能损耗。且需要Android和iOS都写代码配合,且调试复杂。
插件生态不够成熟
插件质量参差不齐,且很多插件与新版本有滞后,生态不如 Android SDK / iOS SDK 成熟。
热更新受限
Flutter不能像 React Native 那样随意热更新
综上,Flutter不适合做强依赖系统能力的应用,也不适合高性能场景的应用如游戏、音视频。
Flutter与原生通信
Android Flutter:手把手教你如何进行Android 与 Flutter的相互通信
Flutter通过PlatformChannel与Android通信,通过FlutterPlatformChannel与iOS通信。
Android的PlatformChannel有三种:
- MethodChannel
- EventChannel
- BasicMessageChannel
Flutter与原生通信是异步消息通信,是把一次调用包装成一条二进制消息,通过BinaryMessenger发到宿主平台;宿主平台收到后再分发到你注册的 channel handler,channel是靠名字匹配的,因此Flutter侧和原生侧channel名字必须一致。
执行完后再把结果编码成二进制回传给Dart,返回用的是回调对象Result,通过如下方式返回结果:
1 | result.success(value) // 成功,Future得到value |
如何将Android的View嵌入到Flutter界面
Flutter侧使用AndroidView,Android侧实现PlatformView,在Flutter引擎中注册。
有Hybrid Composition和Virtual Display两种方式,推荐第一种,第二种是旧方案。
前者是直接把 Android View 加入View层级,后者是创建一个虚拟显示,把View渲染到Texture,Flutter再绘制这个Texture。
Flutter的线程模型
Dart的并发不是Java那种“多个线程共享同一块内存并加锁”,而是Isolate模型。注意线程和Isolate不是一回事。每个Isolate有自己的内存、自己的事件循环,彼此之间主要通过消息机制传递通信。默认情况下,Flutter应用启动后,业务代码、Widget构建、状态变更、动画驱动等,主要都运行在主Isolate上。
这样做的好处是避免传统多线程里的锁竞争、竞态条件和共享状态问题。
Flutter引擎在宿主平台上会使用原生线程去完成平台消息处理、帧调度、栅格化、I/O等工作。Flutter引擎并不是只有一条线程,至少两条关键执行线:UI thread和raster thread。UI thread负责执行Dart代码、build/layout/paint流程、构建 layer tree;raster thread负责把 layer tree 交给图形后端(Skia/Impeller)并最终送到GPU显示。
因此,Dart并发单位为Isolate,Flutter引擎执行单位为多条原生线程。
Dart的运行时模型基于事件循环,类似浏览器JS的事件循环,且都有微队列,不同的是Dart的每个Isolate都有自己的事件循环。
所以Future、async/await本质上是异步调度,它们不会自动开新线程,只是把逻辑挂回事件循环。如果async函数内部还是做了大段同步计算,它依然会阻塞主Isolate。
当一项工作无法在帧间隙里完成时,建议把它挪到另一个isolate,以避免阻塞主isolate。
适合放到新isolate的任务包括:
- 大JSON解析
- 图片处理
- 压缩/解压
- 加密解密
- 大数据排序、过滤、聚合
- 复杂文本处理
- 本地大文件扫描
不太适合放isolate的任务包括:
- 很短的小任务(创建isolate也有成本)
- 必须直接操作 Widget tree / BuildContext 的逻辑
- 频繁、细粒度、低计算量但高通信量的工作
Isolate的通信方式
isolate之间只能通过消息通信。
主要是ReceivePort和SendPort,分别负责收发消息。
单向通信,主isolate创建ReceivePort,然后把它的sendPort发给子isolate,子isolate通过sendPort.send()把结果发回来。
双向通信,在单向通信的基础上,子isolate也会把自己的sendPort传给主isolate,双方能够互发消息。
因此Dart没有共享内存对象可直接改,不能像Java多线程一样直接共享一个List然后两边一起改。且通信本身有成本,如果频繁来回传巨大对象,收益可能被通信/复制成本抵消。所以isolate更适合计算比较重、通信边界比较清晰的场景。