移动开发新利器 | 一文深入了解 Flutter 界面开发

移动开发新利器 | 一文深入了解 Flutter 界面开发

阿里妹导读:谈到移动端开发,大家心中肯定会涌现出一系列名词:iOS、Android、Weex,H5... 那为何还使用 Flutter?其实,Flutter 通过自建绘制引擎,具备与 Native 媲美的性能指数,且有很好的两端一致性,因此 Flutter 提供了一种新的可选项。闲鱼宝贝详情页实践上线也证明了这点,可以在性能无损前提下降低 iOS&Android 开发成本。

本文由闲鱼技术团队出品。它将为你深入介绍 Flutter framework 关于视图树的创建与管理机制、布局、渲染的原理,以及 Flutter 布局与渲染相关性能优化的设计思路的文章。同时介绍在使用 Flutter 开发过程中,遇到的一些坑和相应的解决方案。

Flutter 框架简介

移动开发新利器 | 一文深入了解 Flutter 界面开发

  1. 跨平台应用的框架,没有使用 WebView 或者系统平台自带的控件,使用自身的高性能渲染引擎(Skia)自绘。

  2. 界面开发语言使用 dart,底层渲染引擎使用C, C++。

  3. 组合大于继承,控件本身通常由许多小型、单用途的控件组成,结合起来产生强大的效果,类的层次结构是扁平的,以最大化可能的组合数量。

Rendering Pipeline

移动开发新利器 | 一文深入了解 Flutter 界面开发

本文主要介绍 build、layout、paint 的三个阶段。

视图树

Widget&Element&RenderObject

移动开发新利器 | 一文深入了解 Flutter 界面开发

Flutter 视图树包含了三种树,上图只是介绍了三颗树的基础 class 的对应关系和功能介绍。

创建树

  1. 创建 widget 树

  2. 调用 runApp (rootWidget),将 rootWidget 传给 rootElement,做为 rootElement 的子节点,生成 Element 树,由 Element 树生成 Render 树

    移动开发新利器 | 一文深入了解 Flutter 界面开发

  • Widget:存放渲染内容、视图布局信息,widget 的属性最好都是 immutable (如何更新数据呢?查看后续内容)

  • Element:存放上下文,通过 Element 遍历视图树,Element 同时持有 Widget 和 RenderObject

  • RenderObject:根据 Widget 的布局属性进行 layout,paint Widget 传人的内容

更新树

★为什么 widget 都是 immutable?

Flutter 界面开发是一种响应式编程,主张 simple is fast,Flutter 设计的初衷希望数据变更时发送通知到对应的可变更节点(可能是一个 StatefullWidget 子节点,也可以是 rootWidget),由上到下重新 create widget 树进行刷新,这种思路比较简单,不用关心数据变更会影响到哪些节点。

★widget 重新创建,element 树和 renderObject 树是否也重新创建?

widget 只是一个配置数据结构,创建是非常轻量的,加上 Flutter 团队对 widget 的创建/销毁做了优化,不用担心整个 widget 树重新创建所带来的性能问题,但是 renderobject 就不一样了,renderobject 涉及到 layout、paint 等复杂操作,是一个真正渲染的 view,整个 view 树重新创建开销就比较大,所以答案是否定的。 

★树的更新规则

  1. 找到 widget 对应的 element 节点,设置 element 为 dirty,触发 drawframe, drawframe 会调用 element 的 performRebuild ()进行树重建

  2. widget.build () == null, deactive element.child,删除子树,流程结束

  3. element.child.widget == NULL, mount 的新子树,流程结束

  4. element.child.widget == widget.build () 无需重建,否则进入流程5

  5. Widget.canUpdate (element.child.widget, newWidget) == true,更新 child 的 slot,element.child.update (newWidget)(如果 child 还有子节点,则递归上面的流程进行子树更新),流程结束,否则转6

  6. Widget.canUpdate (element.child.widget, newWidget) != true(widget 的 classtype 或者 key 不相等),deactivew element.child,mount 新子树

注意事项:

  1. element.child.widget == widget.build (),不会触发子树的 update,当触发 update 的时候,如果没有生效,要注意 widget 是否使用旧 widget,没有 new widget,导致 update 流程走到该 widget 就停止了。

  2. 子树的深度变化,会引起子树重建,如果子树是一个复杂度很高的树,可以使用 GlobalKey 做为子树 widget 的 key。GlobalKey 具有缓存功能。

★如何触发树更新

  1. 全局更新:调用 runApp (rootWidget),一般 flutter 启动时调用后不再会调用。

  2. 局部子树更新, 将该子树做 StatefullWidget 的一个子 widget,并创建对应的 State 类实例,通过调用 state.setState () 触发该子树的刷新。

Widget

StatefullWidget vs StatelessWidget

  1. StatelessWidget:无中间状态变化的 widget,需要更新展示内容就得通过重新 new,Flutter 推荐尽量使用 StatelessWidget。

  2. StatefullWidget:存在中间状态变化,那么问题来了,widget 不是都 immutable 的,状态变化存储在哪里?Flutter 引入 state 的类用于存放中间态,通过调用 state.setState ()进行此节点及以下的整个子树更新。

State 生命周期

  1. initState (): state create 之后被 insert 到 tree 时调用的

  2. didUpdateWidget (newWidget):祖先节点 rebuild widget 时调用

  3. deactivate ():widget 被 remove 的时候调用,一个 widget 从 tree 中 remove 掉,可以在 dispose 接口被调用前,重新 instert 到一个新 tree 中

  4. didChangeDependencies ():

  5. 初始化时,在 initState ()之后立刻调用

  6. 当依赖的 InheritedWidget rebuild,会触发此接口被调用

  7. build ():

  8. After calling [initState].

  9. After calling [didUpdateWidget].

  10. After receiving a call to [setState].

  11. After a dependency of this [State] object changes (e.g., an[InheritedWidget] referenced by the previous [build] changes).

  12. After calling [deactivate] and then reinserting the [State] object into the tree at another location.

  13. dispose ():Widget 彻底销毁时调用

  14. reassemble (): hot reload 调用

注意事项:

  1. A页面 push 一个新的页面B,A页面的 widget 树中的所有 state 会依次调用 deactivate (), didUpdateWidget (newWidget)、build ()(这里怀疑是 bug,A页面 push 一个新页面,理论上并没有将A页面进行 remove 操作),当然从功能上,没有看出来有什么异常。

  2. 当 ListView 中的 item 滚动出可显示区域的时候,item 会被从树中 remove 掉,此 item 子树中所有的 state 都会被 dispose,state 记录的数据都会销毁,item 滚动回可显示区域时,会重新创建全新的 state、element、renderobject。

  3. 使用 hot reload 功能时,要特别注意 state 实例是没有重新创建的,如果该 state 中存在一下复杂的资源更新需要重新加载才能生效,那么需要在 reassemble ()添加处理,不然当你使用 hot reload 时候可能会出现一些意想不到的结果,例如,要将显示本地文件的内容到屏幕上,当你开发过程中,替换了文件中的内容,但是 hot reload 没有触发重新读取文件内容,页面显示还是原来的旧内容。

数据流转

★从上往下

数据从根往下传数据,常规做法是一层层往下,当深度变大,数据的传输变的困难,Flutter 提供 InheritedWidget 用于子节点向祖先节点获取数据的机制,如下例子:

移动开发新利器 | 一文深入了解 Flutter 界面开发

child 及其以下的节点可以通过调用下面的接口读取 color 数据:

移动开发新利器 | 一文深入了解 Flutter 界面开发

说明:BuildContext 就是 Element 的一个接口类

移动开发新利器 | 一文深入了解 Flutter 界面开发

context.inheritFromWidgetOfExactType (FrogColor)其实是通过 context/element 往上遍历树,查找到第一个 FrogColor 的祖先节点,取该节点的 widget 对象。

★从下往上

子节点状态变更,向上上报通过发送通知的方式

  • 定义通知类,继承至 Notification

  • 父节点使用 NotificationListener 进行监听捕获通知

  • 子节点有数据变更调用下面接口进行数据上报

移动开发新利器 | 一文深入了解 Flutter 界面开发

★闲鱼 Flutter 的界面框架设计


移动开发新利器 | 一文深入了解 Flutter 界面开发 

Layout 

★Size 计算

移动开发新利器 | 一文深入了解 Flutter 界面开发

parent 传入约束条件,在 dramframe 的 layout 阶段,child 根据自身的渲染内容返回 size。

问题:在 build ()阶段获取不到 size,很多时候需要提前知道部分 widget size 来进行布局,解决方案当 widget 在对应 renderobject 的 layout 阶段之后,发送一个 LayoutChangeNotification,参考 SizeChangedLayoutNotifier class,但是 SizeChangedLayoutNotifier 没有上报 init layout size,可以自己参考这个实现封装一个 Notifier。

★Offset 计算

  1. renderObject 拿到计算好的 size,再加上一些布局属性(align、paddig)等,计算 child 相对 parent 的 offset。

  2. offset 存放在每个 child renderObject 的 BoxParentData 中。

  3. 当 parent 拥有 mutil children 时,BoxParentData 还用来存 children 兄弟节点之间的遍历顺序。 

★Relayout boundary

renderObject 在 layout 阶段做了 Relayout boundary 的优化,当子树进行 relayout 时,满足下面三种中的一种:

  • parentUsesSize == false

  • sizedByParent == true

  • constraints.isTight

那么该 renderObject 设置为 Relayout boundary,也就是该 renderObject 的重新 layout 不触发 parent 的 layout,一般情况下开发人员不需要关心 Relayout boundary,除非是使用 CustomMultiChildLayout。

Paint

★Layer

iOS 的每一个 UIView 都有一个 layer,Flutter 的 render object 不一定存在 layer,一般情况下一个 renderObject 子树都渲染在一个 layer 上,那么什么 renderObject 具有 layer,子 renderObject 怎么渲染到这个 layer?

1. 当一个 renderObject 的移动开发新利器 | 一文深入了解 Flutter 界面开发
或者

移动开发新利器 | 一文深入了解 Flutter 界面开发
,renderOject 会有对应的 compositing layer。

2. 子 renderObject 会对目标 layer 返回对应的 offsetLayer, 目标 compositing layer 再根据 offset 合成一个渲染的纹理 buffer。

移动开发新利器 | 一文深入了解 Flutter 界面开发

★Repaint Boundary

类似 Relayout boundary,Paint 阶段也有 Repaint Boundary,目的和 layout 一样,就是对应子树的 paint 不会导致外部的 repaint,但是 Relayout boundary 需要开发人员自己设置,使用 RepaintBoundary widget 进行设置,ListView 在渲染的 item 默认都是使用了 RepaintBoundary,显而易见 ListView 的 children 之间都是相互独立的。Flutter 建议复杂的 image 渲染使用 RepaintBoundary,image 的渲染需要 io 操作,然后解码,最后渲染,使用 RepaintBoundary 可以进行 gpu 的缓存,但是不一定就会缓存,engine 会判断这个 image 是否足够复杂,毕竟 gpu 缓存还是非常珍贵的,同时 RepaintBoundary 还会对一些反复渲染的 layer 进行缓存处理(反复渲染 3 次及以上,这个是 Flutter 的视频中提到的)。

结语

Flutter 还处于 Beta 阶段,有些界面编程的接口设计还不够成熟,相比 iOS 和安卓生态还很不成熟,需要我们共同的创建,Flutter 提供的调试工具相比一开始接触的时候,已经完善很多,让我们给 Flutter 更多的耐心和包容,期待 Flutter 越来越完善。

参考资料

相关推荐