养车网站开发,jcms网站建设,重庆市建设工程信息网官网公示,中文域名注册费用背景
最近小组在尝试使用一套阿里dinamicX的DSL#xff0c;通过动态模板下发#xff0c;实现Flutter端的动态化模板渲染#xff1b;本来以为只是DSL到Widget的简单映射和数据绑定#xff0c;但实际跑起来的效果出乎意料的差#xff0c;列表卡顿严重#xff0c;帧率丢失严…背景
最近小组在尝试使用一套阿里dinamicX的DSL通过动态模板下发实现Flutter端的动态化模板渲染本来以为只是DSL到Widget的简单映射和数据绑定但实际跑起来的效果出乎意料的差列表卡顿严重帧率丢失严重。这就让我们不得不深入Flutter的Framework层去了解Widget的创建、布局以及渲染的过程。
为什么Native可行的方案在Flutter效果这么差
在iOS和Android开发中DSL到Native的方案其实并不陌生Android中我们就是通过编写XML文件来描述页面布局。Native的这种映射的方案为什么在Flutter上效果变得如此糟糕呢
先通过一个简单的示例来看一下dinamicX DSL的定义 可以看到DSL的设计与Android中的XML很相似在我们的DSL中每个节点的width和height属性可以赋值两种特殊意义的值match_parent和match_content。
match_parent当前节点大小尽量撑开到父节点大小
match_content当前节点大小尽量缩小到容纳子节点大小
在Flutter中并没有match_parent和match_content的概念。最初我们的想法很简单在Widget的build方法中如果属性是match_parent就不断向上遍历直到找到一个父节点有确定的宽高值为止如果是match_content遍历所有的子节点获取子节点大小一旦子节点存在match_content属性会递归调用下去。
表面上看做好每个节点的宽高计算的缓存虽然达不到一次性线性布局这样的开销也并不是很大。但我们忽略掉了一个很重要的问题Widget是immutable的只是包含了视图的配置信息是非常轻量级的。在Flutter中Widget会被不断的创建销毁这会导致布局计算非常的频繁。
要解决这些问题单单处理Widget是不够的需要Element以及RenderObject上做更多的处理这也就是我们为什么要考虑自定义Widget的原因。
接下来通过源码来了解Flutter中Widget的build、layout以及paint相关的逻辑。
认识三棵树
我们通过一个简单的Widget——Opacity来了解一下Widget、Element、RenderObject。
Widget
在Flutter中万物皆是WidgetWidget是immutable的只是包含了视图的配置信息的描述是非常轻量级的创建和销毁的开销比较小。
Opacity继承自RenderObjectWidget其定义了两个比较关键的函数 RenderObjectElement createElement();RenderObject createRenderObject(BuildContext context);这正是我们要找的Element和RenderObject这里只是定义了创建的逻辑具体调用的时机我们继续往下看。
Element
在SingleChildRenderObjectWidget可以看到创建了SingleChildRenderObjectElement对象。
Element是Widget的抽象在Widget初始化的时候调用Widget.createElement创建Element持有Widget和RenderObjectBuildOwner通过遍历Element Tree根据是否标记为dirty构建RenderObject Tree在整个视图构建过程中起到了串联Widget和RenderObject的作用。
RenderObject
Opacity的createRenderObject函数创建了RenderOpacity对象RenderObject真正提供给Engine层渲染所需要的数据RenderOpacity的Paint方法中找到了真正绘制的地方 void paint(PaintingContext context, Offset offset) {if (child ! null) {...context.pushOpacity(offset, _alpha, super.paint);}} 通过RenderObject我们可以处理layout、painting以及hit testing。这是我们在自定义Widget处理最多的事情。RenderObject只是定义了布局的接口并未实现布局模型RenderBox为我们提供了2D笛卡尔坐标系下的Box模型协议定义大部分情况下都可以继承于RenderBox通过重载实现一个新的layout实现paint实现以及点击事件处理等
Flutter在Layout过程中的优化
Flutter采用一次布局的方式O(N)的线性时间来做布局和绘制。 如上图所示在一次遍历中父节点调用每个子节点的布局方法将约束向下传递子节点根据约束计算自己的布局并将结果传回给父节点
RelayoutBoundary优化
当一个节点满足如下条件之一该节点会被标记为RelayoutBoundary子节点的大小变化不会影响到父节点的布局
parentUsesSize false父节点的布局不依赖当前节点的大小sizedByParent true当前节点大小由父节点决定constraints.isTight大小为确定的值即宽高的最大值等于最小值parent is not RenderObject如果父节点不是RenderObject子节点layout变化不需要通知父节点更新
RelayoutBoundary的标记子节点大小变化不会通知父节点重新layout重新paint从而提高效率。 Element更新优化
为什么Widget频繁创建销毁不会影响渲染性能呢
Element定义了updateChild的方法最早在Element被创建Framework调用mount的时候以及RenderObject被标记为needsLayout执行RenderObject.performLayout等场景会调用Element的updateChild方法
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {...if (child ! null) {...if (Widget.canUpdate(child.widget, newWidget)) {...child.update(newWidget);...} }
}对于child和newWidget都不为空的情况通过Widget.canUpdate来判断当前child Element是否可以更新而非重现创建的方式update。
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType newWidget.runtimeType oldWidget.key newWidget.key;}
我们可以看到Widget.canUpdate的定义通过runtimeType和key比较来判断如果可以更新更新Element子节点否则deactivate子节点的Element根据newWidget创建新的Element。
我们如何自定义Widget
第一个版本的设计
在第一个版本的设计中我们考虑的比较简单所有的组件都继承与Object实现一个build方法根据DSL转换的nodeData设置Widget的属性 我们用一个简单的例子来看我们以最坏的情况来考虑第一个节点都是match_content属性每一次Widget创建我们需要的布局计算 这样每一次Widget更新顶部节点的大小计算都要深度遍历整个树。如果Widget其中一个节点更新又会怎样呢 答案是全部重新计算一遍因为Widget是immutable的在不断重新创建销毁。在最坏情况会达到O(N2)可想而知一个长列表会表现如何。
第二个版本的设计
第二个版本我们选择自定义Widget、Element以及RenderObject下面是我们一部分组件的类图。 其中虚线框内是我们自定义的Widget组件。从上面的图可以看出我们自定义的Widget大致分为三种类型
只能作为叶子节点的Widget如Image、Text继承自CustomSingleChildLayout可以设置多个子节点的Widget如FrameLayout、LinearLayout继承自CustomMultiChildLayout可滚动的列表类型的Widget如ListLayout、PageLayout继承自CustomScrollView
在自定义的RenderObject中对于点击事件以及paint方法并未做特殊处理都交由组合的Widget处理。
overridebool hitTestChildren(HitTestResult result, {Offset position}) {return child?.hitTest(result, position: position) ?? false;}overridevoid paint(PaintingContext context, Offset offset) {if (child ! null) context.paintChild(child, offset);}如何处理match_content
当前节点的宽高设置为match_content需要先计算子节点的大小然后再计算当前节点的大小。
在实现自定义的RenderObject中我们需要重写performLayout方法performLayout方法中主要的需要做的事
调用所有子节点的layout方法如果sizedByParent为false需要设置自己size的大小
下面以一个child的情况为例如Padding在RenderObject中对于match_content属性的节点在调用child layout方法时将parentUsesSize设置为true然后size根据child.size设置。
这样做的一个好处当child的大小变化的时候自动会将parent设置为needLayoutparent由于被标记为needLayout会在当前Frame的Pipline中重新layout、paint。当然这样也会带来性能的损耗这一点需要特别注意。
overridevoid performLayout() {assert(callback ! null);invokeLayoutCallback(callback);if (child ! null) {child.layout(constraints, parentUsesSize: true);size constraints.constrain(child.size);} else {size constraints.biggest;
}多child的情况可以参考RenderSliverList的内部实现。
如何处理match_parent
如果当前节点的宽高设置为match_parent尽量扩充到父节点大小这种情况下在Constraints向下传递的时候根据父节点的约束无需子节点计算就已经知道自己的大小在RenderObject中为我们提供了一个属性sizedByParent默认为false如果属性设置为match_parent我们会给当前RenderObject的sizedByParent设置为true这样在Constraints向下传递的时子节点已经知道自己的大小无需layout计算在性能上有所提升。
在RenderObject中当sizedByParent设置为true需要重载performResize方法
overridevoid performResize() {size constraints.biggest;}
这里需要注意的一点这种情况下在重载performLayout方法时不要再设置size的大小。
如果绑定的数据发生变化改变sizedByParent之后确保调用markNeedsLayoutForSizedByParentChange方法将当前节点以及他的父节点设置为needsLayout重新计算布局重新绘制。
前后方案对比
在第二个版本的设计中一个Widget渲染需要怎样一个计算过程呢呢 相同的场景在RenderObject中通过performLayout方法将Constraints向下传递child的size计算并且向上传递最终一次遍历就可以完成整个树的layout计算。
如果是上面更新的场景又会如何呢 根据我们上面讲的Element更新过程以及RenderObject的RelayoutBoundary优化可以看出有新的Widget属性变化Element Tree无需重建更新当前Element节点RenderObject在RelayoutBoundary的优化下只需要更少的layout计算。
经过新方案的优化长列表滑动的平均帧率从28提升到了50左右。
目前存在的问题
目前我们在自定义Widget的实现中其实还是存在问题的。如果仔细看上面performLayout的实现我们在调用每个child的layout方法的时候parentUsesSize都设置为true实际上只有当前节点属性为match_content的时候这才是有必要的。目前我们的处理过于简单导致RelayoutBoundary的优化没有真正享受到。所以目前实际的情况是每次Widget的更新都会导致2N次的Layout计算。这也是帧率达不到Flutter页面的其中一个原因这也是我们接下来要解决的问题。
更多优化方向
经过一系列的优化之后页面的卡顿情况终于有所改善卡顿不再特别明显但整体帧率仍然达不到Flutter页面的效果。仍然需要对Flutter有更深入的理解挖掘出过多性能优化的点进一步做一些更精细化的优化。 ListView和ScrollView在Flutter中都有做性能优化处理。但是对于FrameLayout、LieanrLayout这样有多个child的layout无法享受ListView提供的性能优化。我们是否可以借鉴ListView的ViewPort的概念对于超出屏幕的部分不去做layout、paint渲染。当然这需要考虑Engine层layer缓存等情况需要后续进一步的研究。
另外在parentData存储增加数据缓存以减少数据绑定次数方面以及List嵌套List等复杂情况的优化处理也都需要不断探索。
展望
目前我们实现了DSL到Widget的映射这让Flutter动态模板渲染成为了可能。DSL是一种抽象XML只是其中的一种选择未来在不断完善性能的同时还会提升整个方案的抽象能够支持通用的DSL转换沉淀一套通用解决方案更好的通过技术赋能业务。
DSL到Widget的转换只是其中一环从模板的编辑、本地验证、CDN下发、灰度测试、线上监控等整个闭环仍然有很多需要不断打磨和完善的地方。
原文链接 本文为云栖社区原创内容未经允许不得转载。