Flutter 跨平台框架应用实战-2019极光开发者大会

你们好,我是郭树煜,掘金 《Flutter 完整开发实战详解》 系列的做者,Github GSY 系列开源项目的维护人员,系列包括 GSYVideoPlayerGSYGitGithubApp (Flutter \ ReactNative \ Kotlin \ Weex 四大版本)、GSYFlutterBook 电子书等,系列总 star 数在 25k 左右,目前 Github 中国区粉丝数暂居 67 名,主要负责移动端项目开发,大前端方向,主要涉及领域有 Android、Flutter、React Native、Weex 、小程序等等。前端

此次分享的主题主要涉及:移动端跨平台开发的发展Flutter Widget 的实现原理Flutter 的实战技巧Flutter Web的现状 四个方面,而总体主题将围绕 Widget 为中心展开。git

1、移动端跨平台开发的发展

按照惯例,咱们先介绍历史进程,随着用户终端种类的百花齐放,现在跨平台开发已然成为移动领域的热门话题之一,移动端跨平台开发技术的发展,也表明着开发者对于性能、复用、高效上不断的追求。github

移动端的跨平台开发主要有三个阶段,这些阶段的表明框架主要有:CordovaReact NativeFlutter 等,以下图所示,是移动端的跨平台发展历程:web

Cordova

Cordova 做为早期跨平台领域应用最普遍的框架,为前端人员所熟知,其主要原理就是:面试

将 web 代码打包到本地,利用平台的 WebView 进行加载,经过内部约定好的 JS 通信协议,加载和调用具有平台原生能力的插架。canvas

Cordova 让前端开发人员能够快速的构建移动应用,获取平台入口,对早期 web 上欠缺的如摄像机、本地缓存、文件读写等能力进行快速支持。小程序

早期的移动开发市场除了 Android 和 iOS 以外,还有 WindowPhone、黑莓等,Cordova 简单又实用的理念,使得它成为早期热门的跨平台框架,至今仍在更新的 ionic 框架,也是在其基础上进行了封装发展。缓存

React Native

Cordova 虽然实用方便,可是因为 WebView 的性能瓶颈,开发者开始追求更高性能,且具有平台特点的跨平台能力,这时候由 Facebook 开源的 React Native 框架开始引领新潮流。bash

React Native 让 JS 代码运行在框架内置的 JS 引擎(JavaScriptCore)上,利用 JS 引擎实现了跨平台能力,同时又将 JS 控件,对应解析为平台原生控件进行渲染,从而实现性能的优化与提高。markdown

因为 React 框架的盛行, React Native 也开始成为 React 开发人员,将自身能力拓展到应用开发的最佳选择之一。同时 React Native 也是应用开发人员,接触前端的不错尝试。

后来阿里开源的 Weex 框架设计类似,利用了 V8 引擎实现跨平台,不过使用了 Vue 的设计理念,而 Weex 由于种种缘由,最终仍是没能大面积推广开来。

Flutter

事实上 JS Bridge 一样存在性能等限制,Facebook 也在着力优化这一问题,好比 HermesJS 、底层大规模重构等 ,而 JS -> 平台控件映射,也致使了框架和平台耦合过多,在版本兼容和系统升级等问题上让框架维护愈加困难。

这时候谷歌开源了 Flutter它另辟蹊径,只要求平台提供一个 Surface 和一个 Canvas ,剩下的 Flutter 说:“你能够躺下了,咱们来本身动”。

Flutter 的跨平台思路快速让他成为“新贵”,连跨平台界的老大哥 “JS” 语言都“视而不见”,大胆的选择 Dart 也让 Flutter 在前期的推广中饱受争议。

短短两年,不算 PR ,Flutter 的 issue 已经有近 1.8 万的 closed 和 8000+ open , 这表明了它的热度,也表明着它须要面对的问题和挑战。 不支持 Release 模式下的热更新,也让用户更多徘徊于 React Native 不肯尝试。

不过有一点能够肯定的,那就是 Flutter 的版本号上是完全打败了 React Naitve

总结起来,咱们能够看到,移动端跨平台的发展,从单纯的套壳打包,到提供高性能的跨平台控件封装,再到如今的控件与平台脱离的发展。 整个发展历程,就是对 性能、复用、高效 的不断追求。

题外话,什么要学习跨平台?

一、开发成本

我直接学 Java/KotlinObject-C/SwiftJavaScript/CSS 去写各平台的代码能够吗?

固然能够,这样的性能确定最有保证,可是跨平台的主要优点在于代码逻辑的复用,减小各平台同一逻辑,因人而异的开发成本。

二、学习机会

通常状况下,各平台开发者容易局限在本身的领域开发,而做为应用开发者,跨平台是接触另外一平台或领域的过渡机会。

下面开始今天的主题 Flutter ,Flutter 总体涉及的内容不少,因为篇幅问题,本篇咱们的主题总体都围绕一个 Widget 展开。Flutter 做为跨平台 UI 框架,Widget 是其灵魂设定之一。

2、Flutter Widget 的实现原理

Flutter 是 UI 框架,Flutter 内一切皆 Widget ,每一个 Widget 状态都表明了一帧,Widget 是不可变的。 那么 Widget 是怎么工做的呢?

以下图能够看到,是一个简单的 Flutter Widget 页面代码,页面包含了一个标题和容易,那在页面 build 时,它是怎么表绘制出来的呢?同时它是如何保证性能? 而Widget 又是怎么样的一个概念?后面咱们将逐步揭晓。

首先看上图代码,其实如图的代码并非真正的 View 级别代码,它们更像是配置文件。

而要知道 Widget 是如何工做的,这就涉及到 Flutter 的三大金刚: WidgetElementRenderObject 事实上,这三大金刚才能组成了 Flutter Framework 的基础渲染闭环。

如上图所示,当一个 Widget 被“加载“的时候,它并非立刻被绘制出来,而是会对应先建立出它的 Element ,而后经过 ElementWidget 的配置信息转化为 RenderObject 实现绘制。

因此,在 Flutter 中大部分时候咱们写的是 Widget ,可是 Widget 的角色反而更像是“配置文件” ,真正触发工做的实际上是 RenderObject

小结一下这里的关系就是:

  • Widget 是配置文件。
  • Element 是桥梁和仓库。
  • RenderObject 是解析后的绘制和布局。

对应详细的解释就是:

  • 因此咱们写的 Widget,它须要转化为相应的 RenderObject 去工做;
  • Element 持有 WidgetRenderObject ,做为二者的桥梁,并保存着一些状态参数,咱们在 Flutter 框架中常见到的 BuildContext ,其实就是 Element 的抽象
  • 最后框架会将 Widget 的配置信息,转化到 RenderObject 内,告诉 Canvas 应该在哪一个 Rect 内,绘制多大 Size 的数据。

因此 Widget 和咱们之前的布局概念不同,由于 Widget 是不可变的(immutable),且只有一帧,且不是真正工做的对象,每次画面变化,都会致使一些 Widget 从新 build

那到这里,咱们可能就会关心性能的问题,Flutter 是如何保证性能呢?

1.一、Widget 的轻量级

其实就是回归到了 Widget 的定位,做为“配置文件”,Widget 的变化,是否也会致使 ElementRenderObject 也会从新建立?

答案是不必定会Widget 只是一个 “配置文件” 的做用,是很是轻量级的,它的存在,只是起到对 RenderObject 的数据进行配置的做用。

可是 RenderObject 就不同了,它涉及到了 layoutpaint 等真实 的绘制操做,能够认为是一个真正的 “View” ,若是频繁建立就会导性能出现问题。

因此在 Flutter 中,会有一系列的判断,来处理 WidgetRenderObject 转化的性能问题 ,这部分操做一般是在 Element 中进行的 ,例如 updateChild 时,会有以下图所示的判断:

  • element.child.widget == widget.build() 时,就不会触发 update 操做;

  • update 时,canUpdate(element.child.widget, newWidget) 返回 true, Element 才会被更新;(这里代码中的 slot 通常为 Element 对象,有时候会传空)

  • 其余还有利用 isRelayoutBoundaryisRepaintBoundary 等参数,来实现局部的更新判断,好比:当执行 markNeedsPaint() 触发绘制时,会经过 isRepaintBoundary 是否为 true , 往上肯定了更新区域,经过 requestVisualUpdate 方法触发更新往下绘制。

经过 isRepaintBoundary 参数, 对应的 RenderObject 能够组成一个 Layer

因此这就能够解答一些初学者的疑问,嵌套那么多 Widget ,性能会不会有问题?

这也体现出 Flutter 在布局上和其余框架不一样的地方,你写的 Widget 只是配置文件,堆叠嵌套了一堆控件,对最终的 RenderObject 而言,可能只是多几个 OffsetSize 计算而已。

结合上面的理解,能够知道 Widget 大部分时候,其实只是轻量级的配置,对于性能问题,你更须要关心的是 ClipOverlay 、透明合成等行为,由于它们会须要产生 saveLayer 的操做,由于 saveLayer 会清空GPU绘制的缓存。

最后总结个面试点:

  • 同一个 Widget 能够同时描述多个渲染树中的节点,做为配置文件是能够复用的。 WidgetRenderObject 通常状况是一对多的关系。 ( 前提是在 Widget 存在 RenderObject 的状况。)

  • ElementWidget 的某个固定实例,与 RenderObject 一一对应。(前提是在 Element 存在 RenderObject 的状况。)

  • RenderObjectisRepaintBoundary 标示使得它们组成了一个个 Layer 区域。

isRepaintBoundarytrue 时,该区域就是一个可更新绘制区域,而当这个区域造成时,就会新建立一个 Layer 但不是每一个 RenderObject 都会有 Layer , 由于这受 isRepaintBoundary 的影响。

注意,Flutter 中常见的 BuildContext ,其实就是 Element 的抽象,经过 BuildContext ,咱们通常状况就能够对应得到 Element ,也就是拿到了“仓库的钥匙” ,经过 context 就能够去获取 Element 内持有的东西,好比前面所说的 RenderObject ,还有后面咱们会谈到 State 等。

1.2 Widget 的分类

这里咱们将 Widget 分为以下图所示分类:是否存在 State 、是否存在RenderObject

其实还能够按照 RenderBoxRenderSliver 分类,可是篇幅缘由之后再介绍。

1.2.1 是否存在 State

Flutter 中咱们经常使用的 Widget 有: StatelessWidgetStatefulWidget

以下图, StatelessWidget 的代码很简单,由于 Widget 是不可变的,传入的 text 决定了它显示的内容,而且 text 也算是 final 的。

注意图中 DemoPage 有个黄色警告,这是由于咱们定义了 int i = 0 不是 final 致使的,在 StatelessWidget 中, 非 final 的变量起始容易产生误解,由于 Widget 本事就是不可变的。

前面咱们说过 Widget 都是不可变的,在这个基础上, StatefulWidgetState ,帮咱们实现了 Widget 的跨帧绘制 ,也就是在每次 Widget 重构时,能够经过 State 从新赋予 Widget 须要的配置信息,而这里的 State 对象,就是存在每一个 Element 里的。

同时,前面咱们说过,Flutter 内的 BuildContext 其实就是 Element 的抽象,这说明咱们能够经过 context 去获取 Element 内的东西,好比 StateRenderObjectWidget

Widget ancestorWidgetOfExactType
 State ancestorStateOfType
 State rootAncestorStateOfType
 RenderObject ancestorRenderObjectOfType
复制代码

以下图所示,保存在 State 中的 text ,当咱们点击按键时,setState 时它被标志为 "变化了"它能够主动发生改变,保存变量,再也不只是“只读”状态了

1.2.二、容器 Widget/渲染 Widget

在 Flutter 中还有 容器 Widget渲染Widget 的区别,通常状况下:

  • TextSliderListTile 等都是属于渲染 Widget ,其内部主要是 RenderObjectElement ,对应有 RenderObject 参数。

  • StatelessWidget / StatefulWidget 等属于容器 Widget ,其内部使用的是 ComponentElementComponentElement 自己是不存在 RenderObject 的。

因此做为容器 Widget, 获取它们的 RenderObject 时,获取到的是 build 后的树结构里,最上面渲染 WidgetRenderObject

如上图所示 findRenderObject 的实现,最终就是获取 renderObject在遇到 ComponentElement 时,执行的是 element.visitChildren(visit); , 递归直到找到 RenderObjectElement ,再返回它的 renderObject

获取 RenderObject 在 Flutter 里很重要的,由于获取控件的位置和大小等,都须要经过 RenderObject 获取。

1.三、RenderObject

Flutter 中各种 RenderObject 的实现,大多都是颗粒度很细,功能很单一的存在 :

然而接触过 Flutter 的同窗应该知道 Container 这个 WidgetContainer 的功能却不显单一,这是为何呢?

以下图,由于 Container 实际上是容器 Widget ,它只是把其余“单一”的 Widget 作了二次封装,而后经过配置参数来达到 “多功能的效果” 而已。

因此 Flutter 开发中,咱们常常会根据功能定义出各种如 ContinerScaffold 等脚手架模版,实现灵活与复用的界面开发。

回归到 RenderObject ,事实上 RenderObject 还属于比较“低级”的阶段,由于绘制到屏幕上咱们还须要坐标体系和布局协议等,因此 大部分 WidgetRenderObject 会是子类 RenderBox (RenderSliver 例外) ,由于 RenderObject 自己只实现了基础的 layoutpaint ,而绘制到屏幕上,咱们须要的坐标和大小等,这些内容是在 RenderBox 中开始实现。

RenderSliver 主要是在滚动控件中继承使用。

好比控件被绘制在 x=10,y=20 的位置,而后大小由 parent 对它进行约束显示,RenderBox 继承了 RenderObject,在其基础上实现了 笛卡尔坐标系 和布局协议。

这里咱们经过 Offstage 这个 Widget ,看下其 RenderBox 子类的实现逻辑, Offstage 是用于控制 child 是否显示的做用,以下图,能够看到 RenderOffstage 对于 offstage 标志位的内部逻辑:

那么 Flutter 中的布局协议是什么呢?

简单来讲就是 childparent 之间的大小应该怎么显示,由谁决定显示区域。 相信从 Android 到接触 Flutter 的同窗有这样的疑惑, Flutter 中的 match_parentwrap_content 逻辑须要怎么设置?

就咱们从一个简单的代码分析,以下图所示,Row 布局咱们没有设置任何大小,它是怎么肯定自身大小的呢?

咱们翻阅源码,能够发现其实 Flutter 中经常使用的 RowColumn 等其实都是 Flex 的子类,只是对 Flex 作了简单默认配置。

那按照咱们前面的理解,看一个 Widget 的实现逻辑,就应该看它的 RenderObject ,而在 Flex 布对应的 RenderFlex 中,咱们能够看到以下一段代码:

能够看到在布局的时候,RenderFlex 首先要求 constraints != nullFlex 布局的上层中必须存在约束,否则确定会报错。

以后,在布局时,Row 布局的 direction 是横向的,因此 maxMainSize 为上层布局的最大宽度,而后根据咱们配置的 mainAxisSize 的参数:

  • mainAxisSizemax 时,咱们 Row 的横向布局就是 maxMainSize
  • mainAxisSizemin 时,咱们 Row 的横向布局就是 allocatedSize

前面 maxMainSize 咱们知道了是父布局的最大宽度,而 allocatedSize 其实就是 child 的宽度之和。因此结果很明显了:

对于 Row 来讲, mainAxisSizemax 时就是 match_parentmainAxisSizemin 时就是 wrap_content

而高度 crossSize实际上是由 math.max(crossSize, _getCrossSize(child)); 决定,也就是 child 中最高的一个做为其高度。

最后小结一个知识点:

布局通常都是由上层往下传递 Constraints ,而后由下往上返回 Size

那如何直接自定义 RenderObject 布局?

抛开 Flutter 为咱们封装的好的,三大金刚 WidgetElementRednerObject 一个很多,固然, Flutter 内置了不少封装帮咱们节省代码。

通常状况下自定义 RenderObject 布局:

  • 咱们会继承 MultiChildRenderObjectWidgetRenderBox 这两个 abstract 类,实现本身的WidgetRenderObject 对象;
  • 而后利用 MultiChildRenderObjectElement 关联起它们;
  • 除此以外,还有几个关键的类: ContainerRenderObjectMixinRenderBoxContainerDefaultsMixinContainerBoxParentData 等能够帮你减小代码量。

总结起来, 对于 Flutter 而言,整个屏幕都是一块画布,咱们经过各类 OffsetRect 肯定了位置,而后经过 Canvas 绘制上去,目标是整个屏幕区域,整个屏幕就是一帧,每次改变都是从新绘制。

这里没有介绍 RenderSliver 相关,它的输入和输出和 Renderbox 又不大同样,有机会咱们后面再详细介绍。

3、Flutter 的实战技巧

3.一、InheritedWidget

InheritedWidget 是 Flutter 的灵魂设定之一。

InheritedWidget 共享的是 Widget ,只是这个 Widget 是一个 ProxyWidget ,它本身自己并不绘制什么,但共享这个 Widget 内保存有的数据,从而到了共享状态的目的。

以下图所示,是 Flutter 中常见的 Theme ,其内部就是使用了 _InheritedTheme 这个 InheritedWidget 来实现主题的全局共享的。那么 InheritedWidget 是如何实现全局共享的呢?

其实在 Element 的内部有一个 Map<Type, InheritedElement> _inheritedWidgets; 参数,_inheritedWidgets 通常状况下是空的,只有当父控件是 InheritedWidget 或者自己是 InheritedWidget 时,它才会被初始化,而当父控件是 InheritedWidget 时,这个 Map 会被一级一级往下传递与合并。

因此当咱们经过 context 调用 inheritFromWidgetOfExactType 时,就能够经过这个 Map 往上查找,从而找到这个上级的 InheritedWidget 。(毕竟 context is Element

如咱们的 Theme/ThemeDataText/DefaultTextStyleSlider / SliderTheme 等,以下代码所示,咱们能够定义全局的 ThemeData 或者局部的 DefaultTextStyle ,从而实现全局的自定义和局部的自定义共享等。

其实,Flutter 中大部分的状态管理控件,其状态共享方法,也是基于 InheritedWidget 去实现的。

3.二、支持原生控件

前面咱们说过, Flutter 既然不依赖于原生控件,那么如何集成一些平台已有的控件呢?好比 WebViewMap

咱们这里以 WebView 为例子:

在官方 WebView 控件支持出来以前 ,第三方是直接在 FlutterView 上覆盖了一个新的原生控件,利用 Dart 中的占位控件传递位置和大小

以下图,在 Flutter 端 push 出一个 设定好位置和大小SingleChildRenderObjectWidget ,从而获得须要显示的大小和位置,将这些信息经过 MethodChannel 传递到原生层,在原生层 addContentView 一个指定大小和位置的 WebView

这时候 WebViewSingleChildRenderObjectWidget 处于同样的大小和位置,而空白部分则用 FLutter 的 Appbar 显示。

这样看起来就像是在 Flutter 中添加了 WebView ,但实际这脱离了 Flutter 的渲染树,其中一个问题就是,当你跳转 Flutter 其余页面的时候,会被 WebView 挡住;而且打开页面的动画,AppbarWebView 难以保持一致。

后面 官方 WebView 控件支持出来后,这时候官方是利用 PlatformView 的设计,完成了不脱离 Flutter 渲染堆栈,也能集成平台原生控件的功能。

以 Android 为例,Android 上是利用了副屏显示的底层逻辑,使用 VirtualDisplay 类,建立一个虚拟显示器,须要调用 DisplayManagercreateVirtualDisplay() 方法,将虚拟显示器的内容渲染在一个内存的 Surface 上 ,生成一个惟一的 textureId

以下图,以后渲染时将 textureId 传递给 Dart 层,渲染引擎会根据 textureId , 获取到内存里已渲染数据,绘制到 AndroidView 上进行显示。

3.三、错误处理

Flutter 中比较有趣的状况是,在 Dart 中的一些错误,并不会致使应用闪退,而是经过以下的红色堆栈 UI ,错误区域不一样,多是全屏红,也可能局部红,这种状态就和传统 APP 的“崩溃”状态不大同样了。

在开发过程当中这样的显示没太大问题,但事实发布线上版本就不合适了,因此咱们通常会选择自定义错误显示。

以下图所示,通常咱们能够经过以下处理,自定义咱们的错误页面,而且收集错误信息。

重写 ErrorWidgetbuilder 方法,而后将信息收集到 Zone 中,返回本身的自定义错误显示,最后在 Zone 内利用 onError 统一处理错误。

ps 图中的 Zone 等概念这里就不展开了,有兴趣的能够去之前的文章详细查看。

4、Flutter Web

最后简单说下 Flutter Web ,Flutter 在支持 Web 平台上的优点在于 Flutter UI 与平台的耦合度很低,而 Dart 起初就是为了 Web 而生,一拍即合下 Flutter 支持 Web 并非什么意外。

可是 Web 平台就绕不过 JS ,在 Web 平台,实际上 Image 控件最后会经过 dart2js 转化为 <img> 标签并经过 src 赋值显示。

同时,多了一个平台就多了须要兼容的,目前 Flutter 的 issue 仍然很多,而 Web 支持虽然已经合并到主项目中,可是在兼容、性能等问题上还须要继续优化,好比 Flutter Web 中 canvas.drawColor(Colors.black, BlendMode.clear); 是会出现运行错误的,由于不支持 BlendMode.clear

资源推荐

其余文章

《Flutter完整开发实战详解系列》

《移动端跨平台开发的深度解析》

《全网最全Flutter与React Native深刻对比分析》