Flutter笔记

Flutter与其他跨平台方案的区别

ReactNative之类的框架,只是通过JavaScript虚拟机扩展调用系统组件,由AndroidiOS系统进行组件的渲染;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)、处理输入(触摸、键盘)、提供系统能力(相机、蓝牙等)。

flutter架构

StatefulWidget生命周期

Stateful 组件的生命周期​

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与原生混合开发

Flutter通过PlatformChannel与Android通信,通过FlutterPlatformChannel与iOS通信。

Android的PlatformChannel有三种:

  • MethodChannel
  • EventChannel
  • BasicMessageChannel

Flutter与原生通信是异步消息通信,是把一次调用包装成一条二进制消息,通过BinaryMessenger发到宿主平台;宿主平台收到后再分发到你注册的 channel handler,channel是靠名字匹配的,因此Flutter侧和原生侧channel名字必须一致。

执行完后再把结果编码成二进制回传给Dart,返回用的是回调对象Result,通过如下方式返回结果:

1
2
3
result.success(value)          // 成功,Future得到value
result.error(code, msg, data) // 失败,Future抛PlatformException
result.notImplemented() // 未实现,Future抛MissingPluginException

如何将Android的View嵌入到Flutter界面

Flutter侧使用AndroidView,Android侧实现PlatformView,在Flutter引擎中注册。

Hybrid CompositionVirtual Display两种方式,推荐第一种,第二种是旧方案。

前者是直接把 Android View 加入View层级,后者是创建一个虚拟显示,把View渲染到Texture,Flutter再绘制这个Texture。

Flutter的线程模型

Dart的并发不是Java那种“多个线程共享同一块内存并加锁”,而是Isolate模型注意线程Isolate不是一回事。每个Isolate有自己的内存、自己的事件循环,彼此之间主要通过消息机制传递通信。默认情况下,Flutter应用启动后,业务代码、Widget构建、状态变更、动画驱动等,主要都运行在主Isolate上。

这样做的好处是避免传统多线程里的锁竞争、竞态条件和共享状态问题。

Flutter引擎在宿主平台上会使用原生线程去完成平台消息处理、帧调度、栅格化、I/O等工作。Flutter引擎并不是只有一条线程,至少两条关键执行线:UI threadraster threadUI 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之间只能通过消息通信。

主要是ReceivePortSendPort,分别负责收发消息。

单向通信,主isolate创建ReceivePort,然后把它的sendPort发给子isolate,子isolate通过sendPort.send()把结果发回来。

双向通信,在单向通信的基础上,子isolate也会把自己的sendPort传给主isolate,双方能够互发消息。

因此Dart没有共享内存对象可直接改,不能像Java多线程一样直接共享一个List然后两边一起改。且通信本身有成本,如果频繁来回传巨大对象,收益可能被通信/复制成本抵消。所以isolate更适合计算比较重、通信边界比较清晰的场景。