Flutter 跨平台框架应用实战 -2019 极光开发者大会,音视频开发面试
事实上 JS Bridge
同样存在性能等限制,Facebook 也在着力优化这一问题,比如 HermesJS
、底层大规模重构等 ,而 JS -> 平台控件映射,也导致了框架和平台耦合过多,在版本兼容和系统升级等问题上让框架维护越发困难。
这时候谷歌开源了 `F
lutter,**它另辟蹊径,只要求平台提供一个
Surface和一个
Canvas,剩下的
Flutter` 说:“你可以躺下了,我们来自己动”。**
Flutter
的跨平台思路快速让他成为“新贵”,连跨平台界的老大哥 “JS” 语言都“视而不见”,大胆的选择 Dart
也让 Flutter
在前期的推广中饱受争议。
短短两年,不算 PR ,
Flutter
的 issue 已经有近 1.8 万的 closed 和 8000+ open , 这代表了它的热度,也代表着它需要面对的问题和挑战。 不支持 Release 模式下的热更新,也让用户更多徘徊于 React Native 不愿尝试。不过有一点可以确定的,那就是
Flutter
的版本号上是彻底战胜了React Naitve
。
总结起来,我们可以看到,移动端跨平台的发展,从单纯的套壳打包,到提供高性能的跨平台控件封装,再到现在的控件与平台脱离的发展。 整个发展历程,就是对 性能、复用、高效 的不断追求。
题外话,什么要学习跨平台?
1、开发成本
我直接学 Java
/Kotlin
、Object-C
/Swift
、JavaScript
/CSS
去写各平台的代码可以吗?
当然可以,这样的性能肯定最有保证,但是跨平台的主要优势在于代码逻辑的复用,减少各平台同一逻辑,因人而异的开发成本。
2、学习机会
一般情况下,各平台开发者容易局限在自己的领域开发,而作为应用开发者,跨平台是接触另一平台或领域的过渡机会。
下面开始今天的主题 Flutter ,Flutter 整体涉及的内容很多,由于篇幅问题,本篇我们的主题整体都围绕一个
Widget
展开。Flutter 作为跨平台 UI 框架,Widget
是其灵魂设定之一。
二、Flutter Widget 的实现原理
Flutter 是 UI 框架,Flutter 内一切皆 Widget
,每个 Widget
状态都代表了一帧,Widget
是不可变的。 那么 Widget
是怎么工作的呢?
如下图可以看到,是一个简单的 Flutter Widget
页面代码,页面包含了一个标题和容易,那在页面 build
时,它是怎么表绘制出来的呢?同时它是如何保证性能? 而Widget
又是怎么样的一个概念?后面我们将逐步揭晓。
首先看上图代码,其实如图的代码并不是真正的 View
级别代码,它们更像是配置文件。
而要知道 Widget
是如何工作的,这就涉及到 Flutter 的三大金刚: Widget
、 Element
、RenderObject
。 事实上,这三大金刚才能组成了 Flutter Framework 的基础渲染闭环。
如上图所示,当一个 Widget
被“加载“的时候,它并不是马上被绘制出来,而是会对应先创建出它的 Element
,然后通过 Element
将 Widget
的配置信息转化为 RenderObject
实现绘制。
所以,在 Flutter 中大部分时候我们写的是 Widget
,但是 Widget
的角色反而更像是“配置文件” ,真正触发工作的其实是 RenderObject
。
小结一下这里的关系就是:
Widget
是配置文件。Element
是桥梁和仓库。RenderObject
是解析后的绘制和布局。
对应详细的解释就是:
所以我们写的
Widget
,它需要转化为相应的RenderObject
去工作;Element
持有Widget
和RenderObject
,作为两者的桥梁,并保存着一些状态参数,我们在 Flutter 框架中常见到的BuildContext
,其实就是Element
的抽象 ;最后框架会将
Widget
的配置信息,转化到RenderObject
内,告诉Canvas
应该在哪个Rect
内,绘制多大Size
的数据。
所以 Widget
和我们以前的布局概念不一样,因为 Widget
是不可变的(immutable
),且只有一帧,且不是真正工作的对象,每次画面变化,都会导致一些 Widget
重新 build
。
那到这里,我们可能就会关心性能的问题,Flutter 是如何保证性能呢?
1.1、Widget 的轻量级
其实就是回归到了 Widget
的定位,作为“配置文件”,Widget
的变化,是否也会导致 Element
和 RenderObject
也会重新创建?
答案是不一定会,Widget
只是一个 “配置文件” 的作用,是非常轻量级的,它的存在,只是起到对 RenderObject
的数据进行配置的作用。
但是 RenderObject
就不一样了,它涉及到了 layout
、paint
等真实 的绘制操作,可以认为是一个真正的 “View” ,如果频繁创建就会导性能出现问题。
所以在 Flutter 中,会有一系列的判断,来处理 Widget
到 RenderObject
转化的性能问题 ,这部分操作通常是在 Element
中进行的 ,例如 updateChild
时,会有如下图所示的判断:
当
element.child.widget == widget.build()
时,就不会触发update
操作;在
update
时,canUpdate(element.child.widget, newWidget)
返回 true,Element
才会被更新;(这里代码中的slot
一般为Element
对象,有时候会传空)其他还有利用
isRelayoutBoundary
、isRepaintBoundary
等参数,来实现局部的更新判断,比如:当执行 markNeedsPaint() 触发绘制时,会通过isRepaintBoundary
是否为true
, 往上确定了更新区域,通过requestVisualUpdate
方法触发更新往下绘制。
通过
isRepaintBoundary
参数, 对应的RenderObject
可以组成一个Layer
。
所以这就可以解答一些初学者的疑问,嵌套那么多 Widget
,性能会不会有问题?
这也体现出 Flutter 在布局上和其他框架不同的地方,你写的 Widget
只是配置文件,堆叠嵌套了一堆控件,对最终的 RenderObject
而言,可能只是多几个 Offset
和 Size
计算而已。
结合上面的理解,可以知道 Widget
大部分时候,其实只是轻量级的配置,对于性能问题,你更需要关心的是 Clip
、Overlay
、透明合成等行为,因为它们会需要产生 saveLayer
的操作,因为 saveLayer
会清空 GPU 绘制的缓存。
最后总结个面试点:
同一个
Widget
可以同时描述多个渲染树中的节点,作为配置文件是可以复用的。Widget
和RenderObject
一般情况是一对多的关系。 ( 前提是在Widget
存在RenderObject
的情况。)Element
是Widget
的某个固定实例,与RenderObject
一一对应。(前提是在Element
存在RenderObject
的情况。)RenderObject
内isRepaintBoundary
标示使得它们组成了一个个Layer
区域。
当 isRepaintBoundary
为 true
时,该区域就是一个可更新绘制区域,而当这个区域形成时,就会新创建一个 Layer
。 但不是每个 RenderObject
都会有 Layer
, 因为这受 isRepaintBoundary
的影响。
注意,Flutter 中常见的
BuildContext
,其实就是Element
的抽象,通过BuildContext
,我们一般情况就可以对应获得Element
,也就是拿到了“仓库的钥匙” ,通过context
就可以去获取Element
内持有的东西,比如前面所说的RenderObject
,还有后面我们会谈到State
等。
1.2 Widget 的分类
这里我们将 Widget
分为如下图所示分类:是否存在 State
、是否存在RenderObject
。
其实还可以按照
RenderBox
和RenderSliver
分类,但是篇幅原因以后再介绍。
1.2.1 是否存在 State
Flutter 中我们常用的 Widget
有: StatelessWidget
和 StatefulWidget
。
如下图, StatelessWidget
的代码很简单,因为 Widget
是不可变的,传入的 text
决定了它显示的内容,并且 text
也算是 final
的。
注意图中
DemoPage
有个黄色警告,这是因为我们定义了int i = 0
不是 final 导致的,在StatelessWidget
中, 非 final 的变量起始容易产生误解,因为Widget
本事就是不可变的。
前面我们说过 Widget
都是不可变的,在这个基础上, StatefulWidget
的 State
,帮我们实现了 Widget
的跨帧绘制 ,也就是在每次 Widget
重构时,可以通过 State
重新赋予 Widget
需要的配置信息,而这里的 State
对象,就是存在每个 Element
里的。
同时,前面我们说过,Flutter 内的
BuildContext
其实就是Element
的抽象,这说明我们可以通过context
去获取Element
内的东西,比如State
、RenderObject
、Widget
。
Widget ancestorWidgetOfExactTypeState ancestorStateOfTypeState rootAncestorStateOfTypeRenderObject ancestorRenderObjectOfType
如下图所示,保存在 State
中的 text
,当我们点击按键时,setState
时它被标志为 "变化了"
, 它可以主动发生改变,保存变量,不再只是“只读”状态了。
1.2.2、容器 Widget/渲染 Widget
在 Flutter 中还有 容器 Widget 和 渲染 Widget 的区别,一般情况下:
Text
、Slider
、ListTile
等都是属于渲染Widget
,其内部主要是RenderObjectElement
,对应有RenderObject
参数。StatelessWidget
/StatefulWidget
等属于容器Widget
,其内部使用的是ComponentElement
,ComponentElement
本身是不存在RenderObject
的。
所以作为容器 Widget
, 获取它们的 RenderObject
时,获取到的是 build
后的树结构里,最上面渲染 Widget 的 RenderObject
。
如上图所示
findRenderObject
的实现,最终就是获取renderObject
,在遇到ComponentElement
时,执行的是element.visitChildren(visit);
, 递归直到找到RenderObjectElement
,再返回它的renderObject
。
获取 RenderObject
在 Flutter 里很重要的,因为获取控件的位置和大小等,都需要通过 RenderObject
获取。
1.3、RenderObject
Flutter 中各类 RenderObject
的实现,大多都是颗粒度很细,功能很单一的存在 :
然而接触过 Flutter 的同学应该知道 Container
这个 Widget
,Container
的功能却不显单一,这是为什么呢?
如下图,因为 Container
其实是容器 Widget ,它只是把其他“单一”的 Widget 做了二次封装,然后通过配置参数来达到 “多功能的效果” 而已。
所以 Flutter 开发中,我们经常会根据功能定义出各类如 Continer
、Scaffold
等脚手架模版,实现灵活与复用的界面开发。
回归到 RenderObject
,事实上 RenderObject
还属于比较“低级”的阶段,因为绘制到屏幕上我们还需要坐标体系和布局协议等,所以 大部分 Widget
的 RenderObject
会是子类 RenderBox
(RenderSliver
例外) ,因为 RenderObject
本身只实现了基础的 layout
和 paint
,而绘制到屏幕上,我们需要的坐标和大小等,这些内容是在 RenderBox
中开始实现。
RenderSliver
主要是在滚动控件中继承使用。
比如控件被绘制在 x=10,y=20
的位置,然后大小由 parent
对它进行约束显示,RenderBox
继承了 RenderObject
,在其基础上实现了 笛卡尔坐标系
和布局协议。
这里我们通过 Offstage
这个 Widget
,看下其 RenderBox
子类的实现逻辑, Offstage
是用于控制 child
是否显示的作用,如下图,可以看到 RenderOffstage
对于 offstage
标志位的内部逻辑:
那么 Flutter 中的布局协议是什么呢?
简单来说就是 child
和 parent
之间的大小应该怎么显示,由谁决定显示区域。 相信从 Android 到接触 Flutter 的同学有这样的疑惑, Flutter 中的 match_parent
和 wrap_content
逻辑需要怎么设置?
就我们从一个简单的代码分析,如下图所示,Row
布局我们没有设置任何大小,它是怎么确定自身大小的呢?
我们翻阅源码,可以发现其实 Flutter 中常用的 Row
、Column
等其实都是 Flex
的子类,只是对 Flex
做了简单默认配置。
那按照我们前面的理解,看一个 Widget
的实现逻辑,就应该看它的 RenderObject
,而在 Flex
布对应的 RenderFlex
中,我们可以看到如下一段代码:
可以看到在布局的时候,RenderFlex
首先要求 constraints != null
,Flex
布局的上层中必须存在约束,不然肯定会报错。
之后,在布局时,Row
布局的 direction
是横向的,所以 maxMainSize
为上层布局的最大宽度,然后根据我们配置的 mainAxisSize
的参数:
当
mainAxisSize
为max
时,我们Row
的横向布局就是maxMainSize
;当
mainAxisSize
为min
时,我们Row
的横向布局就是allocatedSize
;
前面 maxMainSize
我们知道了是父布局的最大宽度,而 allocatedSize
其实就是 child 的宽度之和。所以结果很明显了:
对于 Row
来说, mainAxisSize
为 max
时就是 match_parent
;mainAxisSize
为 min
时就是 wrap_content
。
而高度 crossSize
,其实是由 math.max(crossSize, _getCrossSize(child));
决定,也就是 child
中最高的一个作为其高度。
最后小结一个知识点:
布局一般都是由上层往下传递 Constraints
,然后由下往上返回 Size
。
那如何直接自定义 RenderObject
布局?
抛开 Flutter 为我们封装的好的,三大金刚 Widget
、Element
、RednerObject
一个不少,当然, Flutter 内置了很多封装帮我们节省代码。
评论