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 内置了很多封装帮我们节省代码。











评论