当前位置: 首页 > 科技观察

移动开发新工具-深入理解Flutter界面开发

时间:2023-03-19 14:28:04 科技观察

阿里妹攻略:说到移动端开发,大家脑海中肯定会出现一系列名词:iOS、Android、Weex、H5……那为什么要用Flutter?事实上,Flutter通过自建的渲染引擎,拥有媲美Native的性能指标,并且两端一致性好,因此Flutter提供了一个新的选择。闲鱼宝贝详情页的实际上线也证明了这一点,可以在不影响性能的情况下降低iOS&Android开发成本。  本文由闲鱼技术团队制作。将深入介绍Flutterframework关于视图树的创建和管理机制的文章,布局和渲染的原理,以及Flutter布局和渲染相关的性能优化的设计思路。同时介绍了在使用Flutter开发过程中遇到的一些陷阱以及相应的解决方法。  FlutterFramework简介跨平台应用框架,不使用WebView或系统平台自带控件,使用自带的高性能渲染引擎(Skia)进行自绘。界面开发语言使用dart,底层渲染引擎使用C、C++。组合大于继承,控件本身通常由许多小的、单一用途的控件组成,这些控件组合起来产生强大的效果,类层次结构被扁平化以最大化可能组合的数量。  RenderingPipeline  这篇文章主要介绍build、layout和paint三个阶段。  viewtree  Widget&Element&RenderObject  Flutterviewtree包含三种树。上图只介绍了三棵树的基本类的对应关系和功能介绍。  创建树创建widget树调用runApp(rootWidget),将rootWidget作为rootElement的子节点传递给rootElement,生成Element树,从Element树生成Render树Widget:存储渲染内容,查看布局信息,widgetattributes***两者都是不可变的(如何更新数据?见后续内容)Element:存储上下文,通过Element遍历视图树,Element同时持有Widget和RenderObjectRenderObject:根据Widget的layout属性,更新paintWidget的内容  Tree  ★为什么所有的widget都是不可变的?  Flutter界面开发是一种响应式编程,提倡简单就是快速。Flutter设计的初衷是当数据发生变化时,向对应的可变节点(可能是StatefullWidget子节点或rootWidget)发送通知。接下来,重新创建要刷新的小部件树。这个思路比较简单,不需要关心哪些节点会受到数据变化的影响。?★widget重新创建,element树和renderObject树也重新创建?  Widget只是一个配置数据结构,其创建非常轻量级。另外Flutter团队对widget的创建/销毁进行了优化,不用担心重新创建整个widget树带来的性能问题,但是renderobject不同,renderobject涉及到复杂的操作比如布局和油漆。是真实渲染的视图,重新创建整个视图树的成本比较高,所以答案是否定的。★树更新规则找到widget对应的element节点,将element设置为dirty,触发drawframe,drawframe会调用element的performRebuild()重建树widget.build()==null,deactiveelement.child,删除childTree,处理结束element.child.widget==NULL,挂载新的子树,处理结束element.child.widget==widget.build()不需要重建,否则进入处理5Widget.canUpdate(element.child.widget,newWidget)==true,更新child的slot,element.child.update(newWidget)(如果child还有子节点,以上过程递归更新子树),流程结束,否则转6Widget.canUpdate(element.child.widget,newWidget)!=true(widget的classtype或key不相等),deactivewelement.child,挂载新的子树  注意:element.child.widget==widget.build(),不会触发子树的更新。触发更新时,如果没有生效,注意widget是否使用旧的widget,没有新的widget,导致更新过程走到widget时就停止了。子树的深度变化会引起子树的重建。如果子树是一棵复杂度高的树,可以使用GlobalKey作为子树widget的key。GlobalKey具有缓存功能。  ★如何触发树更新全局更新:调用runApp(rootWidget),一般flutter启动后不会调用。更新局部子树,使子树成为StatefullWidget的子Widget,并创建对应的State类实例,通过调用state.setState()触发子树的刷新。  Widget  StatefullWidgetvsStatelessWidgetStatelessWidget:对于没有中间状态变化的widget,如果需要更新显示内容,则需要更新显示内容。Flutter推荐尽量使用StatelessWidget。StatefullWidget:有一个中间的状态变化,那么问题来了,不是所有的widget都是不可变的,状态变化存储在哪里?Flutter引入的state类用于存储中间状态,通过调用state.setState()更新本节点及以下的整个子树。  状态生命周期  initState():didUpdateWidget(newWidget)state创建后插入树时调用:祖先节点重建widget时调用deactivate():移除widget时调用,从树中移除一个widget,可以在调用dispose接口之前,重新insert到一棵新树didChangeDependencies():在初始化时,在initState()之后立即调用依赖的InheritedWidgetrebuild,会触发这个接口调用build():在调用[initState]之后。在调用[didUpdateWidget]之后。在收到对[setState]的调用后。此[State]对象的依赖项发生更改后(例如,先前[build]更改引用的[InheritedWidget])。在调用[deactivate]然后将[State]对象重新插入树的另一个位置之后。dispose():Widget完全销毁时调用reassemble():hotreload调用  注意:A页面推送了一个新的B页面,A页面的widget树中的所有状态都会依次调用deactivate()、didUpdateWidget(newWidget)、build()(这个怀疑是bug,A页面推送了一个新页面,理论上A页面是没有remove操作的),当然从功能上来说,也没有异常。当ListView中的item滚出可显示区域时,item将从树中移除,item子树中的所有状态将被dispose,状态记录的数据将被销毁。当item回滚到可显示区域时,会重新创建全新的state、element、renderobject。在使用热重载功能时,要特别注意状态实例不会被重新创建。如果状态中有一些复杂的资源更新需要重新加载才能生效,那么就需要在reassemble()中加入处理,否则当你使用hotreload时,可能会出现一些意想不到的结果。比如你想在屏幕上显示本地文件的内容,当你在开发过程中替换了文件中的内容,但是热重载没有触发重新读取文件内容,页面显示仍然是原来的旧内容。  数据流  ★自上而下  数据从根到下传输。常规的方法是一层一层往下走。当深度变大时,数据的传输变得困难。Flutter提供了InheritedWidget用于子节点从祖先节点获取数据的机制,如下例所示:  child及其后面的节点可以通过调用如下接口读取颜色数据:  注:BuildContext是一个接口元素?context的类。inheritFromWidgetOfExactType(FrogColor)其实就是通过context/element向上遍历树,找到第一个FrogColor的祖先节点,并取这个节点的widget对象。  ★从下到上  子节点状态变化,向上上报通过发送通知定义通知类,从Notification父节点继承,使用NotificationListener监听、捕获并通知子节点数据变化,调用如下接口上报数据  接口闲鱼Flutter的框架设计Layout    ★Size计算  parent的输入约束。dramframe布局阶段,child根据自己的渲染内容返回尺寸。  问题:在build()阶段获取不到size。很多时候,需要提前知道一些widget的尺寸,以便于布局。解决方案是当小部件处于相应渲染对象的布局阶段时发送LayoutChangeNotification。参考SizeChangedLayoutNotifier类,但是SizeChangedLayoutNotifier并没有Reportinitlayoutsize,可以参考这个实现封装一个Notifier。  ★Offset计算renderObject得到计算出来的size,加上一些布局属性(align,paddig)等,计算child相对parent的偏移量。偏移量存储在每个子renderObject的BoxParentData中。当parent有多个children时,BoxParentData也用来存储children的兄弟节点之间的遍历顺序。    ★Relayout边界  renderObject在布局阶段对Relayout边界进行了优化。当子树重新布局时,满足以下三种类型之一:parentUsesSize==falsesizeByParent==trueconstraints.isTight?然后将renderObject设置为Relayoutboundary,即renderObject的重新布局不会触发parent的布局。一般开发者不需要关心Relayout的边界,除非使用了CustomMultiChildLayout。?Paint  ★Layer  iOS的每个UIView都有层,Flutter的渲染对象不一定有层。一般一个renderObject子树是在一个layer上渲染的,那么什么renderObject都有一个layer,子renderObject怎么渲染到这个layer上呢?1.当一个renderObject或者,renderOject都会有一个对应的合成层。2.子renderObject会返回对应的offsetLayer给目标层,目标合成层会根据偏移量合成一个渲染纹理缓冲区。?★重绘边界?和Relayout边界类似,Paint阶段也有RepaintBoundary。目的和layout一样,就是子树对应的paint不会引起外部repaint,但是Relayout边界需要开发者自己设置。使用RepaintBoundary小部件对其进行设置。ListView渲染的item默认是使用RepaintBoundary,很明显ListView的children是相互独立的。Flutter推荐使用RepaintBoundary进行复杂的图像渲染。图像渲染需要io操作,然后解码,最后渲染。RepaintBoundary可以用来缓存gpu,但不一定一定要缓存。引擎会判断图像是否足够复杂。毕竟,gpu的缓存还是很珍贵的,RepaintBoundary也会缓存一些重复渲染的层(重复渲染3次以上,这个在Flutter视频中有提到)。?结束语  Flutter目前还处于Beta阶段,一些接口编程的接口设计还不够成熟。与iOS、Android生态相比,还不成熟,需要大家共同打造。Flutter提供的调试工具,对比初接触。到时候已经改进了很多,让我们给Flutter更多的耐心和宽容,期待Flutter的改进。  参考https://github.com/flutter/flutterhttps://github.com/flutter/enginehttps://flutter.io/