最近小组在尝试使用集团DinamicX的DSL,经过下发DSL模板,实现Flutter端的动态化模板渲染。咱们解决了性能方面的问题后,又面临了一个新的挑战——渲染一致性。咱们该如何在不下降渲染性能的前提下,大幅度提高Flutter与Native之间的渲染一致性呢?node
在第一版渲染架构设计当中,咱们以Widget为中心,采用了组合的方案来完成DSL到Widget的转化。这方面的工做在早期还算比较顺利,然而随着模板复杂度的增长,逐渐出现了一些Bad Case。服务器
咱们分析了这些Bad Case后发现,在第一版渲染架构下,没法完全解决这些Bad Case,缘由主要为如下两点:架构
如需从根本上解决这些问题,咱们须要从新设计一套新的渲染架构方案,彻底理解并对齐DSL的布局理念。ide
因为DinamicX的DSL与Android XML十分类似,所以咱们将以Android的Measure机制来介绍其布局理念。相信不少同窗都明白,在Android的Measure机制中,父View会根据自身的MeasureSpecMode和子View的LayoutParams来计算出子View的MeasureSpecMode,其具体计算表格以下(忽略了MeasureSpecMode为UNSPECIFIED的状况):布局
咱们能够基于上面这个表格,计算出每一个DSL Node的宽/高是EXACTLY仍是AT_MOST的。 Flutter若想理解DynamicX DSL,就须要引入MeasureSpecMode的概念。因为第一版渲染架构以Widget为中心,难以引入MeasureSpecMode的概念,于是咱们须要以RenderObject为中心,对渲染架构作从新的设计。性能
咱们基于RenderObject层,设计了一个新的渲染架构。在新的渲染架构中,每个DSL Node都会被转化为RenderObject Tree上的一颗子树,这棵子树主要由三部分组成。测试
Render层为咱们新版渲染架构中的核心层,用于表达Node转化后的布局规则与尺寸大小,对于理解DSL布局理念起到了关键性做用,其类图以下:优化
DXRenderBox是全部控件Render层的基类,其派生了两个类:DXSingleChildLayoutRender和DXMultiChildLayoutRender。其中DXSingleChildLayoutRender是全部非布局控件Render层的基类,而DXMultiChildLayoutRender则是全部布局控件Render层的基类。ui
对于非布局控件来讲,Render层只会影响其尺寸,不影响内部显示的内容,因此理论上View、ImageView、Switch、Checkbox等控件在Render层的表达都是相同的。DXContainerRender就是用于表达这些非布局控件的实现类。这里TextView因为有maxWidth属性会影响其尺寸以及须要特殊处理文字垂直居中的状况,于是单独设计了DXTextContainerRender。this
对于布局控件来讲,不一样的布局控件表明着不一样的布局规则,所以不一样的布局控件在Render层会派生出不一样的实现类。DXLinearLayoutRender和DXFrameLayoutRender分别用于表达LinearLayout与FrameLayout的布局规则。
完成新版渲染架构设计以后,咱们能够开始设计咱们的基类DXRenderBox了。对于DXRenderBox来讲,咱们须要实现它在Flutter Layout中很是关键的三个方法:sizedByParent、performResize和performLayout。
咱们先来简单回顾一下Flutter Layout的原理,因为以前已有诸多文章介绍过Flutter Layout的原理,咱们此次就直接聚焦于Flutter Layout中用于计算RenderObject的size的部分。
在Flutter Layout的过程当中,最为重要的就是肯定每一个RenderObject的size,而size的肯定是在RenderObject的layout方法中完成的。layout方法主要作了两件事:
为了方便读者阅读,咱们将layout方法作了简化,代码以下:
abstract class RenderObject { Constraints get constraints => _constraints; Constraints _constraints; bool get sizedByParent => false; void layout(Constraints constraints, { bool parentUsesSize = false }) { //计算relayoutBoundary ...... //layout _constraints = constraints; if (sizedByParent) { performResize(); } performLayout(); ...... } }
能够说只要掌握了layout方法,那么对于Flutter Layout的过程也就基本掌握了。接下来咱们来简单分析一下layout方法。
参数constraints表明了parent传入的约束,最后计算获得的RenderObject的size必须符合这个约束。参数parentUsesSize表明parent是否会使用child的size,它参与计算repaintBoundary,能够对Layout过程起到优化做用。
sizedByParent是RenderObject的一个属性,默认为false,子类能够去重写这个属性。顾名思义,sizedByParent表示RenderObject的size的计算彻底由其parent决定。换句话说,也就是RenderObject的size只和parent给的constraints有关,与本身children的sizes无关。
同时,sizedByParent也决定了RenderObject的size须要在哪一个方法中肯定,若sizedByParent为true,那么size必须得在performResize方法中肯定,不然size须要在performLayout中肯定。
performResize方法的做用是肯定size,实现该方法时须要根据parent传入的constraints肯定RenderObject的size。
performLayout则除了用于肯定size之外,还须要负责遍历调用child.layout方法对计算children的sizes和offsets。
sizedByParent为true时,表示RenderObject的size与children无关。那么在咱们的DXRenderBox中,只有当widthMeasureMode和heightMeasureMode均为DX_EXACTLY时,sizedByParent才能被设为true。
代码中的nodeData类型为DXWidgetNode,表明上文中提到的DSL Node,而widthMeasureMode和heightMeasureMode则分别表明DSL Node的宽与高对应的MeasureSpecMode。
abstract class DXRenderBox extends RenderBox { DXRenderBox({@required this.nodeData}); DXWidgetNode nodeData; @override bool get sizedByParent { return nodeData.widthMeasureMode == DXMeasureMode.DX_EXACTLY && nodeData.heightMeasureMode == DXMeasureMode.DX_EXACTLY; } ...... }
只有sizedByParent为true时,也就是widthMeasureMode和heightMeasureMode均为DX_EXACTLY时,performResize方法才会被调用。而若widthMeasureMode和heightMeasureMode均为DX_EXACTLY,则证实nodeData的宽高要么是具体值,要么是match_parent,因此在performResize方法里,咱们只须要处理宽/高为具体值或match_parent的状况便可。宽/高有具体值取具体值,没有具体值则表示其为match_parent,取constraints的最大值。
abstract class DXRenderBox extends RenderBox { ...... @override void performResize() { double width = nodeData.width ?? constraints.maxWidth; double height = nodeData.height ?? constraints.maxHeight; size = constraints.constrain(Size(width, height)); } ...... }
DXRenderBox做为全部控件Render层的基类,无需实现performLayout。不一样的DXRenderBox的子类对应的performLayout方法是不一样的,这个方法也是Flutter理解DSL的关键。接下来咱们以DXSingleChildLayoutRender为例子来讲明performLayout的实现思路。
DXSingleChildLayoutRender的主要做用是肯定非布局控件的大小。好比一个ImageView具体有多大,就是经过它来肯定的。
abstract class DXSingleChildLayoutRender extends DXRenderBox with RenderObjectWithChildMixin<RenderBox> { @override void performLayout() { BoxConstraints childBoxConstraints = computeChildBoxConstraints(); if (sizedByParent) { child.layout(childBoxConstraints); } else { child.layout(childBoxConstraints, parentUsesSize: true); size = defaultComputeSize(child.size); } } ...... }
首先,咱们先计算出childBoxConstraints。接着判断DXSingleChildLayoutRender是不是sizedByParent。若是是,那么DXSingleChildLayoutRender的size已经在performResize阶段计算完成,此时只须要调用child.layout方法便可。不然,咱们须要在调用child.layout时将parentUsesSize参数设置为true,经过child.size来计算DXSingleChildLayoutRender的size。但是咱们该如何根据child.size来计算DXSingleChildLayoutRender的size呢?
Size defaultComputeSize(Size intrinsicSize) { double finalWidth = nodeData.width ?? constraints.maxWidth; double finalHeight = nodeData.height ?? constraints.maxHeight; if (nodeData.widthMeasureMode == DXMeasureMode.DX_AT_MOST) { finalWidth = intrinsicSize.width; } if (nodeData.heightMeasureMode == DXMeasureMode.DX_AT_MOST) { finalHeight = intrinsicSize.height; } return constraints.constrain(Size(finalWidth,finalHeight)); }
1)若是宽/高所对应的measureMode为DX_EXACTLY,那么最终宽/高则有具体值取具体值,没有具体值则表示其为match_parent,取constraints的最大值。
2)若是宽/高所对应的measureMode为DX_ATMOST,那么最终宽/高取child的宽/高便可。
布局控件在performLayout中除了须要肯定本身的size之外,还须要设计好本身的布局规则。咱们以FrameLayout为例来讲明一下布局控件的performLayout该如何实现。
class DXFrameLayoutRender extends DXMultiChildLayoutRender { @override void performLayout() { BoxConstraints childrenBoxConstraints = computeChildBoxConstraints(); double maxWidth = 0.0; double maxHeight = 0.0; //layout children visitDXChildren((RenderBox child,int index,DXWidgetNode childNodeData,DXMultiChildLayoutParentData childParentData) { if (sizedByParent) { child.layout(childrenBoxConstraints,parentUsesSize: true); } else { child.layout(childrenBoxConstraints,parentUsesSize: true); maxWidth = max(maxWidth,child.size.width); maxHeight = max(maxHeight,child.size.height); } }); //compute size if (!sizedByParent) { size = defaultComputeSize(Size(maxWidth, maxHeight)); } //compute children offsets visitDXChildren((RenderBox child,int index,DXWidgetNode childNodeData,DXMultiChildLayoutParentData childParentData) { Alignment alignment = DXRenderCommon.gravityToAlignment(childNodeData.gravity ?? nodeData.childGravity); childParentData.offset = alignment.alongOffset(size - child.size); }); } }
FrameLayout的布局过程一共可分为3部分
看了FrameLayout的布局过程,是否以为很是简单呢?不过须要指出的是,上述FrameLayoutRender的代码会遇到一些Bad Case,其中比较经典的问题就是FrameLayout的宽/高为match_content,而其children的宽/高均为match_parent。这种状况在Android下会对同一个child进行"两次measure",那么在Flutter下,咱们该如何实现呢?
咱们先来看一个例子:
上图的LinearLayout是一个竖向线性布局,width被设为了match_content,它包含了两个TextView,width均为match_parent,那么这个例子中,整个布局的流程应该是怎样的呢。
首先须要依次measure两个TextView的width,MeasureSpecMode为AT_MOST,简单来讲,就是问它们具体须要多宽。接着LinearLayout会将两个TextView须要的宽度的最大值设为本身的宽度。最后,对两个TextView进行第二次measure,此时MeasureSpecMode会被改成Exactly,MeasureSpecSize为LinearLayout的宽度。
而常见的Flutter的layout过程为如下两种:
以上方案均不能知足例子中咱们想要的效果,咱们须要找到一个方案,在调用child.layout以前,便能知道child的宽高。最后咱们发现,getMinIntrinsicWidth、getMaxIntrinsicWidth、getMinIntrinsicHeight、getMaxIntrinsicHeight四个方法可以知足咱们。咱们以getMaxIntrinsicHeight为例,来说讲这些方法的用途。
double getMaxIntrinsicWidth(double height) { return _computeIntrinsicDimension(_IntrinsicDimension.maxWidth, height, computeMaxIntrinsicWidth); }
getMaxIntrinsicWidth接收一个参数height,用于肯定当height为这个值时maxIntrinsicWidth应该是多少。这个方法最终会经过computeMaxIntrinsicWidth方法来计算maxIntrinsicWidth,计算结果会被保存。若是咱们须要重写,不该该重写getMaxIntrinsicWidth方法,而是应该重写computeMaxIntrinsicWidth方法。须要注意的是这些方法并不是轻量级方法,只有在真正须要的时候才可以使用。
或许你不由要问,这些方法计算出来的宽高准吗?实际上每一个RenderBox的子类都须要保证这些方法的正确性,好比用于展现文字的RenderParagraph就实现了这些compute方法,所以咱们得以在RenderParagraph没被layout以前,获取其宽度。
咱们设计的Render层中的类也得实现compute方法,这些方法实现起来并不复杂,咱们仍是以DXSingleChildLayoutRender为例子来讲明该如何实现这些方法。
@override double computeMaxIntrinsicWidth(double height) { if (nodeData.width != null) { return nodeData.width; } if (child != null) return child.getMaxIntrinsicWidth(height); return 0.0; }
上述代码比较简单,再也不赘述。
那么咱们能够来解决例子中的问题了。咱们先经过child.getMaxIntrinsicWidth来计算每一个child须要的width。接着咱们将这些宽度的最大值肯定LinearLayout的width,最后咱们经过child.layout对每一个孩子进行布局,传入的constraints的maxWidth和minWidth均为LinearLayout的width。
新版渲染架构使得Flutter能理解并对齐DSL的布局理念,系统性解决了以前遇到的Bad Case,为Flutter动态模板方案带来了更多的可能性。
咱们对新老版本的渲染性能作了测试对比,在新版渲染架构下,咱们经过页面渲染耗时对比以及FPS对比能够发现,动态模板的渲染性能获得了进一步的提高。
在渲染架构升级以后,咱们完全解决了以前遇到的Bad Case,并为系统性分析解决这类问题提供了有力的抓手,还进一步提高了渲染性能,这让Flutter动态模板渲染成为了可能。将来咱们将继续完善这套解决方案,作到技术赋能业务。
双11福利来了!先来康康#怎么买云服务器最便宜# [并不简单]参团购买指定配置云服务器仅86元/年,开团拉新享三重礼:1111红包+瓜分百万现金+31%返现,爆款必买清单,还有iPhone 11 Pro、卫衣、T恤等你来抽,立刻来试试手气!https://www.aliyun.com/1111/2019/home?utm_content=g_1000083110
本文做者:闲鱼技术
本文为云栖社区原创内容,未经容许不得转载。