flutter开发初探
作为时下最火的跨端技术,虽然现在才能才入局有点晚的感觉,但是本人是喜欢稳定版的,目前1.22.x也已经官方release了,这篇初探就简单记录下,一枚小白的使用心得和入门吧
国际惯例
摆出这张图,还是简单从整体上来先认识了一下什么是 Flutter,否则容易陷入“盲人摸象”的境地。
- Embedder 操作系统适配层,提供线程模型,事件循环模型
- Engine:和底层OS无关了,一般是渲染层包括了 Skia 图形绘制库、Dart VM、Text 等,其中 Skia 和 Text 为上层接口提供了调用底层渲染和排版的能力
- Framework:是一个用 Dart 实现的 UI SDK,从上之下包括了两大风格组件库(iOS和Android)、基础组件库、图形绘制、手势识别、动画等功能
FlutterEngine它主要负责包括Dart虚拟机、动画和图形、文字渲染、通信通道、事件通知、插件架构等。引擎渲染采用的是2D图形渲染库Skia,虚拟机采用的是面向对象语言Dart VM,并将它们托管到平台的中间层代码(Embedder)。 这里提一个名词:Dart Isolate 虚拟机中的任何 Dart 代码都是运行在一些 独立分区(isolate) 中,它可以被很好地解释为有着自己的堆(heap) 通常还带着自己的控制线程(mutator thread) 的独立 Dart 宇宙。它可以并行地执行许多块独立分区的 Dart 代码,只是不能直接分享任何状态,仅可以通过 端口(prots)(不要和网络端口搞混了) 进行消息传递来沟通。
我们通常说的dart是单线程实际上是指我们在一个main isolate里面执行(runApp(main)),实际上dart可以开启多个isolate 但是dart isolate不同于线程,是不互相共享内存的。
Flutter绘制
首先是用户操作,触发 Widget Tree 的更新,然后构建 Element Tree,计算重绘区后将信息同步给 RenderObject Tree,之后实现组件布局、组件绘制、图层合成、引擎渲染。
渲染过程中有3棵树比较重要:
Widget Tree, Element Tree, RenderObject Tree
Widget Tree
基本逻辑单位,是用户对界面 UI 的描述方式。其实Widget是不可变的,只是通过重绘来更新state
Element Tree
它是 Widget 的实例化对象,createElement
工厂方法来创建Element。
Element Tree 的重新创建和重新渲染的开销会非常大, 所以 Element Tree 到 RenderObject Tree 也有一个 Diff 环节,来计算最小重绘区域。
需要注意的是,Element 同时持有 Widget 和 RenderObject, 但无论是 Widget 还是 Element,其实都不负责最后的渲染,它们只是“发号施令”,真正对配置信息进行渲染的是 RenderObject。
RenderObject Tree
RenderObject Tree 在 Flutter 的展示过程分为四个阶段:
- 布局
- 绘制
- 合成
- 渲染
其中,布局和绘制在 RenderObject 中完成,Flutter 采用深度优先机制遍历渲染对象树,确定树中各个对象的位置和尺寸,并把它们绘制到不同的图层上。绘制完毕后,合成和渲染的工作则交给 Skia 处理。
理论上可以直接让Widget和RenderObject通信,不过因为Widget设计为不可变的,但是最终在屏幕上的object不可能一直不变。如果每次改变都去全局渲染object,会损耗大量性能。所以Element实际上是对Widget做了抽象,只将变化的部分通知Render层,由此最大程度去降低重绘区域,提高渲染效率。
Flutter绘制流程拆解
- Build
- Diff
- Layout
- Paint
- Composite
- Render
自绘引擎
- 通过Skia直接调用OpenGL渲染,保证性能同时抹平差异。
- Dart同时支持JIT和AOT。
Flutter混合开发
混合模式
-
统一管理模式
所谓统一管理模式,就是一个标准的 Flutter Application 工程,而其中 Flutter 的产物工程目录(
ios/
和android/
)是可以进行原生混编的工程,如 React Native 进行混合开发那般,在工程项目中进行混合开发就好。但是这样的缺点是当原生项目业务庞大起来时,Flutter 工程对于原生工程的耦合就会非常严重,当工程进行升级时会比较麻烦。因此这种混合模式只适用于 Flutter 业务主导、原生功能为辅的项目。 -
三端分离模式
后来 Google 对混合开发有了更好的支持,除了 Flutter Application,还支持 Flutter Module。所谓 Flutter Module,恰如其名,就是支持以模块化的方式将 Flutter 引入原生工程中, 它的产物就是 iOS 下的 Framework 或 Pods、Android 下的 AAR,原生工程就像引入其他第三方 SDK 那样,使用 Maven 和 Cocoapods 引入 Flutter Module 即可。 从而实现真正意义上的三端分离的开发模式。
混合栈原理
混合导航栈主要需要解决以下四种场景下的问题:
-
Native 2 Native
这种情况比较简单,Flutter Engine 已经为我们提供了现成的 Plugin,即 iOS 下的 FlutterViewController 与 Android 下的 FlutterView(自行包装一下可以实现 FlutterActivity),所以这种场景我们直接使用启动了的 Flutter Engine 来初始化 Flutter 容器,为其设置初始路由页面之后,就可以以原生的方式跳转至 Flutter 页面了。
-
Flutter 2 Flutter
- 使用 Flutter 本身的 Navigator 导航栈
- 创建新的 Flutter 容器后,使用原生导航栈
-
Flutter 2 Native
这里的跳转其实是包含了两种情况,一是打开原生页面(open,包括但不限于 push),二是回退到原生页面(close,包括但不限于 pop)。
-
Native 2 Native
Widget
Flutter 中是通过 Widget 嵌套 Widget 的方式来构建UI和进行实践处理的,其实它的功能就是描述一个UI元素的配置信息。并不是表示最终绘制在设备屏幕上的显示元素,所谓的配置信息就是 Widget 接收的参数,比如对于 Text 来讲,文本的内容、对齐方式、文本样式都是它的配置信息。 网上有张图描述的widget我觉得比较好,见下:
- 「Component Widget」 —— 组合类 Widget,这类 Widget 都直接或间接继承于StatelessWidget或StatefulWidget,Widget 设计上遵循组合大于继承的原则,通过组合功能相对单一的 Widget 可以得到功能更为复杂的 Widget。平常的业务开发主要是在开发这一类型的 Widget;
- 「Proxy Widget」 —— 代理类 Widget,正如其名,「Proxy Widget」本身并不涉及 Widget 内部逻辑,只是为「Child Widget」提供一些附加的中间功能。典型的如:InheritedWidget用于在「Descendant Widgets」间传递共享信息、ParentDataWidget用于配置「Descendant Renderer Widget」的布局信息;
- 「Renderer Widget」 —— 渲染类 Widget,是最核心的Widget类型,会直接参与后面的「Layout」、「Paint」流程,无论是「Component Widget」还是「Proxy Widget」最终都会映射到「Renderer Widget」上,否则将无法被绘制到屏幕上。这 3 类 Widget 中,只有「Renderer Widget」有与之一一对应的「Render Object」
三棵树
Flutter 框架的的处理流程是这样的:
- 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自 Element 类。
- 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自RenderObject 类。
- 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自 Layer 类。
真正的布局和渲染逻辑在Render树种,Element是Widget和RenderObject的粘合剂,可以理解为一个中间代理
StatelessWidget,StatelessWidget相对比较简单,它继承自widget类,重写了createElement()方法
context
build方法有一个context参数,它是BuildContext类的一个实例,表示当前 widget 在 widget 树中的上下文,每一个 widget 都会对应一个 context 对象(因为每一个 widget 都是 widget 树上的一个节点)。实际上,context是当前 widget 在 widget 树中位置中执行”相关操作“的一个句柄(handle),比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。
- StatefulElement 间接继承自Element类,与StatefulWidget相对应(作为其配置数据)。StatefulElement中可能会多次调用createState()来创建状态(State)对象。
- createState() 用于创建和 StatefulWidget 相关的状态,它在StatefulWidget 的生命周期中可能会被多次调用。例如,当一个 StatefulWidget 同时插入到 widget 树的多个位置时,Flutter 框架就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个StatefulElement对应一个State实例。
State的生命周期
理解State的生命周期对flutter开发非常重要
- initState:当 widget 第一次插入到 widget 树时会被调用,对于每一个State对象,Flutter 框架只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。不能在该回调中调用BuildContext.dependOnInheritedWidgetOfExactType,原因是在初始化完成后, widget 树中的InheritFrom widget也可能会发生变化,所以正确的做法应该在在build()方法或didChangeDependencies()中调用它。
- didChangeDependencies():当State对象的依赖发生变化时会被调用;例如:在之前build() 中包含了一个InheritedWidget ,然后在之后的build() 中Inherited widget发生了变化,那么此时InheritedWidget的子 widget 的didChangeDependencies()回调都会被调用。典型的场景是当系统语言 Locale 或应用主题改变时,Flutter 框架会通知 widget 调用此回调。需要注意,组件第一次被创建后挂载的时候(包括重创建)对应的didChangeDependencies也会被调用。
- build():主要是用于构建 widget 子树的,会在如下场景被调用:
- 在调用initState()之后。
- 在调用didUpdateWidget()之后。
- 在调用setState()之后。
- 在调用didChangeDependencies()之后。
- 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
- reassemble():此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
- didUpdateWidget ():在 widget 重新构建时,Flutter 框架会调用widget.canUpdate来检测 widget 树中同一位置的新旧节点,然后决定是否需要更新,如果widget.canUpdate返回true则会调用此回调。正如之前所述,widget.canUpdate会在新旧 widget 的 key 和 runtimeType 同时相等时会返回true,也就是说在在新旧 widget 的key和runtimeType同时相等时didUpdateWidget()就会被调用。
- deactivate():当 State 对象从树中被移除时,会调用此回调。在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey 来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
- dispose():当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。
通过 RenderObject 自定义 Widget
StatelessWidget 和 StatefulWidget 都是用于组合其它组件的,它们本身没有对应的 RenderObject。 Flutter 最原始的定义组件的方式就是通过定义RenderObject 来实现,而StatelessWidget 和 StatefulWidget 只是提供的两个帮助类。相当于是搭积木。
所以我们也可以通过RenderObject自定义Widget,相当于建积木。 如果组件不会包含子组件,则我们可以直接继承自 LeafRenderObjectWidget ,它是 RenderObjectWidget 的子类,而 RenderObjectWidget 继承自 Widget
就是帮 widget 实现了createElement 方法,它会为组件创建一个 类型为 LeafRenderObjectElement 的 Element对象。如果自定义的 widget 可以包含子组件,则可以根据子组件的数量来选择继承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget,它们也实现了createElement() 方法,返回不同类型的 Element 对象。 具体需要实现 performLayout paint 等方法,大家可以自行查看
状态管理
响应式的编程框架中都会有一个永恒的主题——“状态(State)管理”
- Widget 管理自己的状态。
- Widget 管理子 Widget 状态。
- 混合管理(父 Widget 和子 Widget 都管理状态)。
全局状态管理
正确的做法是通过一个全局状态管理器来处理这种相距较远的组件之间的通信。目前主要有两种办法:
- 实现一个全局的事件总线,将语言状态改变对应为一个事件,然后在APP中依赖应用语言的组件的initState 方法中订阅语言改变的事件。当用户在设置页切换语言后,我们发布语言改变事件,而订阅了此事件的组件就会收到通知,收到通知后调用setState(…)方法重新build一下自身即可。
- 使用一些专门用于状态管理的包,如 Provider、Redux。
- ChangeNotifier:真正数据(状态)存放的地方
- ChangeNotifierProvider:Widget树中提供数据(状态)的地方,会在其中创建对应的ChangeNotifier
- Consumer:Widget树中需要使用数据(状态)的地方
路由管理
路由(Route)在移动开发中通常指页面(Page),这跟 Web 开发中单页应用的 Route 概念意义是相同的,Route 在 Android中 通常指一个 Activity,在 iOS 中指一个 ViewController。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。Flutter 中的路由管理和原生开发类似,无论是 Android 还是 iOS,导航管理都会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。
MaterialPageRoute
MaterialPageRoute继承自PageRoute类,PageRoute类是一个抽象类,针对不同平台,实现与平台页面切换动画风格一致的路由切换动画,最上层是继承route,是一个抽象类。
Navigator
Navigator通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。是一个管理route的widget。
命名路由
简单理解就是有名字的路由,要使用命名路由需要提供并注册一个路由表。
路由钩子
Widget类型的路由路由可以支持onGenerateRoute,做一些全局的路由跳转前置处理逻辑,比如跳转之前判断是否已经登录啊等等
包管理
就是我们pubspec.yaml里面的一些依赖管理。
资源管理
资源配置也放在yaml里:
使用的时候记得带上包名 AssetImage(‘icons/aaa.png’, package: ‘xxx’) Image.asset(‘icons/bbb.png’, package: ‘xxx’)
上述图片都是在flutter中使用的,如果包含native,需要在native中添加图片,如启动图片:
Android:
iOS:
FlutterHotLoad
Flutter有AOT和JIT两种编译模式,正因为JIT(在debug模式下),flutter才能实现热重载。整套过程在DartVM的支撑下完成。
- 代码改动:工具会扫描工程下的文件,通过修改时间来比对哪些文件被修改,通过HTTP端口发送给DartVM,(源码位于/flutter/packages/flutter_tools/lib/src/run_hot.dart); 首次编译:第一次启动会生成全量app.dill文件;
- 增量编译:对修改的文件编译生成app.dill.incremental.dill增量文件,这个dill文件本质上就是一种中间描述;
- 更新文件:将增量产物推送到设备中;
- UI更新:DartVM收到增量文件后进行合并,并通知Flutter引擎更新UI,Flutter framework中BindingBase注册了名为reassemble的Dart VM服务,用于外部与正在运行的Dart VM通信,能够触发根节点树重建操作,服务触发后,BindingBase.reassembleApplication-> WidgetsBinding. performReassemble -> BuildOwner.reassemble -> Element.reassemble 由根节点开始一步步实现widgets树重建。(源码位于/flutter/packages/flutter/lib/src/foundation/binding.dart);
但是不是所有的情况都支持hotLoad的,比如下面几种:
- 代码出现编译错误;
- Widget 状态无法兼容;
- 全局变量和静态属性的更改;
- main 方法里的更改;
- initState 方法里的更改;
- 枚举和泛类型更改。