跨平台开发方案的三个时代
Web容器时代:基于Web相关技术通过浏览器组件来实现界面及功能,典型的框架包括 Cordova(PhoneGap)、Ionic和微信小程序。
缺点:一个完整H5页面的展示要经历浏 览器控件的加载、解析和渲染三大过程,性能消耗要比原生开发增加N个数量级,内存占用多、网页加载速度慢、渲染慢、JavaScript 执行慢、交互体验差等
泛Web容器时代:采用类Web标准进行开发,但在运行时把绘制和渲染交由原生系统接管的技术,代表框 架有React Native、Weex和快应用,广义的还包括天猫的Virtual View等。
缺点:需要处理大量平台相关的逻辑外,随着系统版本变化和API的变化,我们还需要处理不同平台的原生 控件渲染能力差异,修复各类奇奇怪怪的Bug。
自绘引擎时代:自带渲染引擎,客户端仅提供一块画布即可获得从业务逻辑到功能呈现的多端高度一致的 渲染体验。Flutter,是为数不多的代表。
下面是跨平台开发方案的对比:
开发方案 | Web容器 | RN | Weex | Flutter |
---|---|---|---|---|
类型 | Web容器 | 泛Web容器 | 泛Web容器 | 自绘引擎 |
支持平台 | Android/iOS/Web | Android/iOS | Android/iOS/Web | Android/iOS |
开发语言 | Javascript | Javascript(React) | Javascript(Vue) | Dart |
技术栈 | 前端 | 偏前端 | 偏前端 | 偏客户端 |
动态化能力 | 支持 | 支持 | 支持 | 不支持 |
渲染性能 | 差 | 一般 | 一般 | 好 |
滑动性能 | 差 | 一般 | 一般 | 好 |
页面级支持 | 支持 | 支持 | 支持 | 支持 |
区块级支持 | 不支持 | 支持 | 支持 | 支持 |
开发效率 | 高 | 一般 | 一般 | 一般 |
维护成本 | 低 | 一般 | 高 | 低 |
系统升级视频难度 | 低 | 中 | 中 | 低 |
社区生态 | 活跃 | 活跃 | 不活跃 | 活跃 |
Flutter框架介绍
Flutter架构采用分层设计,从下到上分为三层,依次为:Embedder、Engine、Framework。
Embedder是操作系统适配层,实现了渲染Surface设置,线程设置,以及平台插件等平台相关特性的适 配。从这里我们可以看到,Flutter平台相关特性并不多,这就使得从框架层面保持跨端一致性的成本相对 较低。
Engine层主要包含Skia、Dart和Text,实现了Flutter的渲染引擎、文字排版、事件处理和Dart运行时等功 能。Skia和Text为上层接口提供了调用底层渲染和排版的能力,Dart则为Flutter提供了运行时调用Dart和 渲染引擎的能力。而Engine层的作用,则是将它们组合起来,从它们生成的数据中实现视图渲染。
Framework层则是一个用Dart实现的UI SDK,包含了动画、图形绘制和手势识别等功能。为了在绘制控 件等固定样式的图形时提供更直观、更方便的接口,Flutter还基于这些基础能力,根据Material和 Cupertino两种视觉设计风格封装了一套UI组件库。我们在开发Flutter的时候,可以直接使用这些组件库。
Flutter是怎么运转的?
Flutter和其他跨平台方案的本质区别
与用于构建移动应用程序的其他大多数框架不同,Flutter是重写了一整套包括底层渲染逻辑和上层开发语言 的完整解决方案。这样不仅可以保证视图渲染在Android和iOS上的高度一致性(即高保真),在代码执行 效率和渲染性能上也可以媲美原生App的体验(即高性能)。自己完成了组件渲染的闭环
React Native之类的框架,只是通过JavaScript虚拟机扩展调用系统组件,由Android和iOS系统进行组件 的渲染;
Flutter是怎么完成组件渲染的呢?这需要从图像显示的基本原理说起。
在计算机系统中,图像的显示需要CPU、GPU和显示器一起配合完成:CPU负责图像数据计算,GPU负责图 像数据渲染,而显示器则负责最终图像显示。
CPU把计算好的、需要显示的内容交给GPU,由GPU完成渲染后放入帧缓冲区,随后视频控制器根据垂直同 步信号(VSync)以每秒60次的速度,从帧缓冲区读取帧数据交由显示器完成图像显示。
操作系统在呈现图像时遵循了这种机制,而Flutter作为跨平台开发框架也采用了这种底层方案。
可以看到,Flutter关注如何尽可能快地在两个硬件时钟的VSync信号之间计算并合成视图数据,然后通过 Skia交给GPU渲染:UI线程使用Dart来构建视图结构数据,这些数据会在GPU线程进行图层合成,随后交给 Skia引擎加工成GPU数据,而这些数据会通过OpenGL最终提供给GPU渲染。
Skia 是什么?
Skia是一款用C++开发的、性能彪悍的2D图像绘制引擎,其前身是一个向量绘图软件。2005年被Google公 司收购后,因为其出色的绘制表现被广泛应用在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平台上的渲染效果是完全一致的。
Dart 是什么?
Dart 历史
2011年10月,在丹麦召开的GOTO大会上,Google发布了一种新的编程语言Dart。如同Kotlin和Swift的出 现,分别是为了解决Java和Objective-C在编写应用程序的一些实际问题一样,Dart的诞生正是要解决 JavaScript存在的、在语言本质上无法改进的缺陷。
JavaScript因为Node.js焕发了第二春,而Dart就没有那么好的运气了。由于缺少顶级项目的使用,Dart始终 不温不火。2015年,在听取了大量开发者的反馈后,Google决定将内置的Dart VM引擎从Chrome移除,这 对Dart的发展来说是重大挫折,替代JavaScript就更无从谈起了。
在Google内部孵化了移动开发框架Flutter,弯道超车进入了移动开发的领 域;而在Google未来的操作系统Fuchsia中,Dart更是被指定为官方的开发语言。
Dart的特性
核心特性
JIT与AOT:借助于先进的工具链和编译器,Dart是少数同时支持JIT(Just In Time,即时编译)和AOT(Ahead of Time,运行前编译)的语言之一。JIT在运行时即时编译,在开发周期中使用,可以动态下发和执行代码,开发测试效率高,但运行速度和 执行性能则会因为运行时即时编译受到影响。AOT即提前编译,可以生成被直接执行的二进制代码,运行速度快、执行性能表现好,但每次执行前都需 要提前编译,开发测试效率低。
内存分配与垃圾回收:
Dart VM的内存分配策略比较简单,创建对象时只需要在堆上移动指针,内存增长始终是线性的,省去了查 找可用内存的过程。
在Dart中,并发是通过Isolate实现的。Isolate是类似于线程但不共享内存,独立运行的worker。这样的机 制,就可以让Dart实现无锁的快速分配。
Dart的垃圾回收,则是采用了多生代算法。新生代在回收内存时采用“半空间”机制,触发垃圾回收时, Dart会将当前半空间中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存。回收过程中, Dart只需要操作少量的“活跃”对象,没有引用的大量“死亡”对象则被忽略,这样的回收机制很适合 Flutter框架中大量Widget销毁重建的场景。
单线程模型:Dart中并没有线程,只有Isolate(隔离区)。Isolates之间不会共享内存,就像几个运行在不同进程 中的worker,通过事件循环(Event Looper)在事件队列(Event Queue)上传递消息通信。
无需单独的声明式布局语言,界面布局直接通过Dart编码来定义,使得Flutter并不需要类似JSX或XML的声明式布局语言。所有的布 局都使用同一种格式,也使得Flutter很容易提供高级工具使布局更简单。
为什么是Dart?
Google公司给出的原因很简单也很直接:Dart语言开发组就在隔壁,对于Flutter需要的一些语言新特 性,能够快速在语法层面落地实现;而如果选择了JavaScript,就必须经过各种委员会和浏览器提供商漫长的决议。
- 同时支持即时编译JIT和事前编译AOT
- Dart的学习成本并不高,很容易上手,Dart作为一门现代化语言,集百家之长,拥有其他优秀编程语言的诸多特性
- 利用独特的隔离区(Isolate)实现多线程。而且不共享内存,避免了抢占式调度和共享内存,可以在没有锁的情况下进行对象分配和垃圾回收,在性能方面表现相当不错。
- 创建的对象分配内存时,Dart 是在现有的堆上移动指针,保证内存的增长是程线性的,于是就省了查找可用内存的过程。
- google的支持
Flutter 视图树结构
Flutter 的视图结构的抽象分为三部分:Widget, Element, RenderObject
Widget:Widget 里面存储了一个视图的配置信息,可以高效的创建(build)和销毁
Element:是分离 WidgetTree 和真正的渲染对象的中间层, WidgetTree 用来描述对应的Element 属性
RenderObject:来执行 Diff, Hit Test 布局、绘制
视图树的操作
创建树
创建树的过程:创建widget树 -> 调用runApp(rootWidget),将rootWidget传给rootElement,做为rootElement的子节点,生成Element树,由Element树生成Render树
Widget:存放渲染内容、视图布局信息,widget的属性最好都是immutable
Element:存放上下文,通过Element遍历视图树,Element同时持有Widget和RenderObject
RenderObject:根据Widget的布局属性进行layout,paint Widget传人的内容
更新树
widget只是一个配置数据结构,创建是非常轻量的,加上flutter团队对widget的创建/销毁做了优化,不用担心整个widget树重新创建所带来的性能问题,但是renderobject就不一样了,renderobject涉及到layout、paint等复杂操作,是一个真正渲染的view,整个view 树重新创建开销就比较大,所以答案是否定的。
树的更新规则
- 找到widget对应的element节点,设置element为dirty,触发drawframe, drawframe会调用element的
performRebuild()
进行树重建 widget.build() == null, deactive element.child
删除子树,流程结束element.child.widget == NULL, mount
的新子树,流程结束element.child.widget == widget.build()
无需重建,否则进入流程5Widget.canUpdate(element.child.widget, newWidget) == true
更新child的slot,element.child.update(newWidget)(如果child还有子节点,则递归上面的流程进行子树更新),流程结束,否则转6Widget.canUpdate(element.child.widget, newWidget) != true
(widget的classtype 或者 key 不相等),deactivew element.child,mount 新子树
注意:
element.child.widget == widget.build()
,不会触发子树的update,当触发update的时候,如果没有生效,要注意widget是否使用旧widget,没有new widget,导致update流程走到该widget就停止了
子树的深度变化,会引起子树重建,如果子树是一个复杂度很高的树,可以使用GlobalKey做为子树widget的key。GlobalKey具有缓存功能
如何触发树更新
全局更新:调用runApp(rootWidget)
,一般flutter启动时调用后不再会调用
局部子树更新, 将该子树做StatefullWidget的一个子widget,并创建对应的State类实例,通过调用state.setState() 触发该子树的刷新
生命周期
initState()
: state create之后被insert到tree时调用的didUpdateWidget(newWidget)
:祖先节点rebuild widget时调用deactivate()
:widget被remove的时候调用,一个widget从tree中remove掉,可以在dispose接口被调用前,重新instert到一个新tree中didChangeDependencies()
:- 初始化时,在
initState()
之后立刻调用 - 当依赖的InheritedWidget rebuild,会触发此接口被调用
build()
:- After calling [initState].
- After calling [didUpdateWidget].
- After receiving a call to [setState].
- After a dependency of this [State] object changes (e.g., an[InheritedWidget] referenced by the previous [build] changes).
- After calling [deactivate] and then reinserting the [State] object into the tree at another location.
dispose()
:Widget彻底销毁时调用reassemble()
: hot reload调用
注意:
A页面push一个新的页面B,A页面的widget树中的所有state会依次调用deactivate()
,didUpdateWidget(newWidget)
、build()
(这里怀疑是bug,A页面push一个新页面,理论上并没有将A页面进行remove操作),当然从功能上,没有看出来有什么异常
当ListView中的item滚动出可显示区域的时候,item会被从树中remove掉,此item子树中所有的state都会被dispose,state记录的数据都会销毁,item滚动回可显示区域时,会重新创建全新的state、element、renderobject
使用hot reload功能时,要特别注意state实例是没有重新创建的,如果该state中存在一下复杂的资源更新需要重新加载才能生效,那么需要在reassemble()
添加处理,不然当你使用hot reload时候可能会出现一些意想不到的结果,例如,要将显示本地文件的内容到屏幕上,当你开发过程中,替换了文件中的内容,但是hot reload没有触发重新读取文件内容,页面显示还是原来的旧内容
数据流转
从上往下
InheritedWidget用于子节点向祖先节点获取数据的机制
1 | class FrogColor extends InheritedWidget { |
child 及其以下的节点可以通过调用下面的接口读取color数据:FrogColor.of(context).color
context.inheritFromWidgetOfExactType(FrogColor) 其实是通过context/element往上遍历树,查找到第一个FrogColor的祖先节点,取该节点的widget对象
从下往上
通过发送通知的方式
- 定义通知类,继承至Notification
- 父节点使用NotificationListener 进行监听捕获通知
- 子节点有数据变更调用下面接口进行数据上报
Notification(data).dispatch(context)
流程如下:
Layout
Size 计算
parent传入约束条件,在dramframe的layout阶段,child根据自身的渲染内容返回size。但是,有个问题,在build()阶段获取不到size,很多时候需要提前知道部分widget size来进行布局,解决方案当widget 在对应renderobject的layout阶段之后,发送一个LayoutChangeNotification,参考SizeChangedLayoutNotifier class,但是SizeChangedLayoutNotifier没有上报init layout size,可以自己参考这个实现封装一个Notifier
Offset 计算
renderObject拿到计算好的size,再加上一些布局属性(align、paddig)等,计算child相对parent的offset;offset存放在每个child renderObject的BoxParentData中;当parent拥有mutil children时,BoxParentData还用来存children兄弟节点之间的遍历顺序
Relayout boundary
renderObject在layout阶段做了Relayout boundary的优化,当子树进行relayout时,满足下面三种中的一种
1 | parentUsesSize == false |
那么该renderObject设置为Relayout boundary,也就是该renderObject的重新layout不触发parent的layout,一般情况下开发人员不需要关心Relayout boundary,除非是使用CustomMultiChildLayout。
本文完
欢迎关注公众号:flutter_todo,有更多技术干货和学习资源教程分享。