大神教你举一反三---用实战讲述Flutter-跨平台框架应用

首先看上图代码,其实如图的代码并不是真正的 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 ancestorWidgetOfExactType
State ancestorStateOfType
State rootAncestorStateOfType
RenderObject 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 标志位的内部逻辑:
[图片上传失败…(image-6da7e5-1571570507182)]
那么 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 内置了很多封装帮我们节省代码。
一般情况下自定义 RenderObject 布局:
- 我们会继承
MultiChildRenderObjectWidget和RenderBox这两个abstract类,实现自己的Widget和RenderObject对象; - 然后利用
MultiChildRenderObjectElement关联起它们; - 除此之外,还有几个关键的类:
ContainerRenderObjectMixin、RenderBoxContainerDefaultsMixin和ContainerBoxParentData等可以帮你减少代码量。

总结起来, 对于 Flutter 而言,整个屏幕都是一块画布,我们通过各种 Offset 和 Rect 确定了位置,然后通过 Canvas 绘制上去,目标是整个屏幕区域,整个屏幕就是一帧,每次改变都是重新绘制。
这里没有介绍
RenderSliver相关,它的输入和输出和Renderbox又不大一样,有机会我们后面再详细介绍。
三、Flutter 的实战技巧
3.1、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)

结尾
好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划,可以来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。
这里放上一部分我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~


网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
更多推荐
所有评论(0)