写点什么

Flutter 跨平台框架应用实战 -2019 极光开发者大会,音视频开发面试

用户头像
Android架构
关注
发布于: 刚刚

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


这时候谷歌开源了 `F


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


lutter,**它另辟蹊径,只要求平台提供一个Surface和一个Canvas,剩下的Flutter` 说:“你可以躺下了,我们来自己动”。**



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


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

不过有一点可以确定的,那就是 Flutter 的版本号上是彻底战胜了 React Naitve


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

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

1、开发成本


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


当然可以,这样的性能肯定最有保证,但是跨平台的主要优势在于代码逻辑的复用,减少各平台同一逻辑,因人而异的开发成本。


2、学习机会


一般情况下,各平台开发者容易局限在自己的领域开发,而作为应用开发者,跨平台是接触另一平台或领域的过渡机会。


下面开始今天的主题 Flutter ,Flutter 整体涉及的内容很多,由于篇幅问题,本篇我们的主题整体都围绕一个 Widget 展开。Flutter 作为跨平台 UI 框架,Widget 是其灵魂设定之一。

二、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.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 ancestorWidgetOfExactTypeState ancestorStateOfTypeState rootAncestorStateOfTypeRenderObject ancestorRenderObjectOfType


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


1.2.2、容器 Widget/渲染 Widget

在 Flutter 中还有 容器 Widget渲染 Widget 的区别,一般情况下:


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

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


所以作为容器 Widget, 获取它们的 RenderObject 时,获取到的是 build 后的树结构里,最上面渲染 Widget RenderObject



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


获取 RenderObject 在 Flutter 里很重要的,因为获取控件的位置和大小等,都需要通过 RenderObject 获取。

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

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Flutter 跨平台框架应用实战-2019极光开发者大会,音视频开发面试