做一个高一致性、高性能的Flutter动态渲染,真的很难么?

2019-11-06 admin

最近小组在尝试使用集团DinamicX的DSL,通过下发DSL模板实现Flutter端的动态化模板渲染。在解决了性能方面的问题后,又面临了一个新的挑战——渲染一致性。如何在不降低渲染性能的前提下,大幅度提升Flutter与Native之间的渲染一致性呢?

思路

在初版渲染架构设计当中,我们以Widget为中心,采用了组合的方案来完成DSL到Widget的转化。这方面的工作在早期还算比较顺利,然而随着模板复杂度的增加,逐渐出现了一些Bad Case。

分析了这些Bad Case后发现,在初版渲染架构下,无法彻底解决这些Bad Case,原因主要为以下两点:

1. 我们使用了Stack来代表FrameLayout,Column/Row来代表LinearLayout,它们看似功能相似,实则内部实现差异较大,使用过程中引起了很多难以解决的Bad Case。

2. 初版尝试通过自定义Widget对DSL的布局理念做了初步的理解,但是未能做到完全对齐,使得Bad Case无法得到系统性解决。

如需从根本上解决这些问题,需要重新设计一套新的渲染架构方案,完全理解并对齐DSL的布局理念。

新版渲染架构设计

由于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上的一颗子树,这棵子树主要由三部分组成。

  • Decoration层:Decoration层用于支持背景色、边框、圆角、触摸事件等,这些我们可以通过组合方式实现。

  • Render层:Render层用于表达Node在转化后的布局规则与尺寸大小。

  • Content层:Content层负责显示具体内容,对于布局控件来说,内容就是自己的children,而对于非布局控件如TextView、ImageView等,内容将采用Flutter中的RenderParagraph、RenderImage来表达。

Render层为我们新版渲染架构中的核心层,用于表达Node转化后的布局规则与尺寸大小,对于理解DSL布局理念起到了关键性作用,其类图如下:

DXRenderBox是所有控件Render层的基类,其派生了两个类:DXSingleChildLayoutRender和DXMultiChildLayoutRender。其中DXSingleChildLayoutRender是所有非布局控件Render层的基类,而DXMultiChildLayoutRender则是所有布局控件Render层的基类。

对于非布局控件来说,Render层只会影响其尺寸,不影响内部显示的内容,所以理论上View、ImageView、Switch、Checkbox等控件在Render层的表达都是相同的。DXContainerRender就是用于表达这些非布局控件的实现类。这里TextView由于有maxWidth属性会影响其尺寸以及需要特殊处理文字垂直居中的情况,因而单独设计了DXTextContainerRender。

对于布局控件来说,不同的布局控件代表着不同的布局规则,因此不同的布局控件在Render层会派生出不同的实现类。DXLinearLayoutRender和DXFrameLayoutRender分别用于表达LinearLayout与FrameLayout的布局规则。

新版渲染架构实现

完成新版渲染架构设计之后,我们可以开始设计基类DXRenderBox了。对于DXRenderBox来说,我们需要实现它在Flutter Layout中非常关键的三个方法:sizedByParent、performResize和performLayout。

Flutter Layout的原理

我们先来简单回顾一下Flutter Layout的原理,由于之前已有诸多文章介绍过Flutter Layout的原理,这次就直接聚焦于Flutter Layout中用于计算RenderObject的size的部分。

在Flutter Layout的过程中,最为重要的就是确定每个RenderObject的size,而size的确定是在RenderObject的layout方法中完成的。layout方法主要做了两件事:

1. 确定当前RenderObject对应的relayoutBoundary

2. 调用performResize或performLayout去确定自己的size

为了方便读者阅读将layout方法做了简化,代码如下:

abstractclassRenderObject{Constraintsget constraints => _constraints;Constraints _constraints;boolget 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

sizedByParent为true时,表示RenderObject的size与children无关。那么在我们的DXRenderBox中,只有当widthMeasureMode和heightMeasureMode均为DX_EXACTLY时,sizedByParent才能被设为true。

代码中的nodeData类型为DXWidgetNode,代表上文中提到的DSL Node,而widthMeasureMode和heightMeasureMode则分别代表DSL Node的宽与高对应的MeasureSpecMode。

abstractclassDXRenderBoxextendsRenderBox{DXRenderBox({@requiredthis.nodeData});DXWidgetNode nodeData;@overrideboolget sizedByParent {return nodeData.widthMeasureMode == DXMeasureMode.DX_EXACTLY &&            nodeData.heightMeasureMode == DXMeasureMode.DX_EXACTLY;}......}

如何实现performResize

只有sizedByParent为true时,也就是widthMeasureMode和heightMeasureMode均为DXEXACTLY时,performResize方法才会被调用。而若widthMeasureMode和heightMeasureMode均为DXEXACTLY,则证明nodeData的宽高要么是具体值,要么是match parent,所以在performResize方法里只需要处理宽/高为具体值或matchparent的情况即可。宽/高有具体值取具体值,没有具体值则表示其为match_parent,取constraints的最大值。

abstractclassDXRenderBoxextendsRenderBox{......@overridevoid performResize() {double width = nodeData.width ?? constraints.maxWidth;double height = nodeData.height ?? constraints.maxHeight;        size = constraints.constrain(Size(width, height));}......}

非布局空间如何实现performLayout

DXRenderBox作为所有控件Render层的基类,无需实现performLayout。不同的DXRenderBox的子类对应的performLayout方法是不同的,这个方法也是Flutter理解DSL的关键。接下来以DXSingleChildLayoutRender为例子来说明performLayout的实现思路。

DXSingleChildLayoutRender的主要作用是确定非布局控件的大小。比如一个ImageView具体有多大,就是通过它来确定的。

abstractclassDXSingleChildLayoutRenderextendsDXRenderBoxwithRenderObjectWithChildMixin<RenderBox> {@overridevoid performLayout() {BoxConstraints childBoxConstraints = computeChildBoxConstraints();if(sizedByParent) {      child.layout(childBoxConstraints);} else{      child.layout(childBoxConstraints, parentUsesSize: true);      size = defaultComputeSize(child.size);}}......}

首先,我们先计算出childBoxConstraints。接着判断其是否是sizedByParent。如果是,那么其size已经在performResize阶段计算完成,此时只需要调用child.layout方法即可。 否则,需要在调用child.layout时将parentUsesSize参数设置为true,通过child.size来计算其size。可是该如何根据child.size来计算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));}
  • 如果宽/高所对应的measureMode为DXEXACTLY,那么最终宽/高则有具体值取具体值,没有具体值则表示其为matchparent,取constraints的最大值。

  • 如果宽/高所对应的measureMode为DX_ATMOST,那么最终宽/高取child的宽/高即可。

布局空间如何实现performLayout

布局控件在performLayout中除了需要确定自己的size以外,还需要设计好自己的布局规则。以FrameLayout为例来说明一下布局控件的performLayout该如何实现。

classDXFrameLayoutRenderextendsDXMultiChildLayoutRender{@overridevoid 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 sizeif(!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部分

1. layout所有的children,如果FrameLayoutRender不是sizedByParent,需要同时计算所有children的最大宽度与最大高度,用于计算自身size。

2. 计算自身size,其中计算方案defaultComputeSize详见上一小节

3. 将gravity转化为alignment,计算所有children的offsets。

看了FrameLayout的布局过程,是否觉得非常简单呢?不过需要指出的是,上述FrameLayoutRender的代码会遇到一些Bad Case,其中比较经典的问题就是FrameLayout的宽/高为matchcontent,而其children的宽/高均为matchparent。 这种情况在Android下会对同一个child进行"两次measure",那么在Flutter下该如何实现呢?

Flutter如何实现两次measure的问题?

我们先来看一个例子:

上图的LinearLayout是一个竖向线性布局,width被设为了matchcontent,它包含了两个TextView,width均为matchparent,那么这个例子中,整个布局的流程应该是怎样的呢。

首先需要依次measure两个TextView的width,MeasureSpecMode为AT_MOST,简单来说,就是问它们具体需要多宽。接着LinearLayout会将两个TextView需要的宽度的最大值设为自己的宽度。 最后,对两个TextView进行第二次measure,此时MeasureSpecMode会被改为Exactly,MeasureSpecSize为LinearLayout的宽度。

而常见的Flutter的layout过程为以下两种:

  • 先在performResize中计算自身size,再通过child.layout确定children sizes

  • 先通过child.layout确定children sizes,再根据children sizes计算自身size

以上方案均不能满足例子中我们想要的效果,需要找到一个方案,在调用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为例子来说明该如何实现这些方法。

@overridedouble computeMaxIntrinsicWidth(double height) {if(nodeData.width != null) {return nodeData.width;}if(child != null) return child.getMaxIntrinsicWidth(height);return0.0;}

上述代码比较简单,不再赘述。

那么我们再简单看一下例子中的问题——先通过child.getMaxIntrinsicWidth来计算每个child需要的width。接着将这些宽度的最大值确定LinearLayout的width,最后通过child.layout对每个孩子进行布局,传入的constraints的maxWidth和minWidth均为LinearLayout的width。

效果

新版渲染架构使得Flutter能理解并对齐DSL的布局理念,系统性解决了之前遇到的Bad Case,为Flutter动态模板方案带来了更多的可能性。

对新老版本的渲染性能做了测试对比,在新版渲染架构下通过页面渲染耗时对比以及FPS对比可以发现,动态模板的渲染性能得到了进一步的提升。

后续展望

在渲染架构升级之后,我们彻底解决了之前遇到的Bad Case,并为系统性分析解决这类问题提供了有力的抓手,还进一步提升了渲染性能,这让Flutter动态模板渲染成为了可能。未来我们将继续完善这套解决方案,做到技术赋能业务。

参考文献

https://flutter.dev/docs/resources/inside-flutter

https://www.youtube.com/watch?v=UUfXWzp0-DU

https://www.youtube.com/watch?v=dkyY9WCGMi0

闲鱼团队是Flutter+Dart FaaS前后端一体化新技术的行业领军者,就是现在! 客户端/服务端java/架构/前端/质量工程师面向社会招聘,base杭州阿里巴巴西溪园区,一起做有创想空间的社区产品、做深度顶级的开源项目,一起拓展技术边界成就极致!

投喂简历给小闲鱼→guicai.gxy@alibaba-inc.com*

开源项目、峰会直击、关键洞察、深度解读请认准 闲鱼技术

[转载]原文链接:https://juejin.im/entry/5dc10c275188255f8268a5ed

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-78860.html

文章标题:做一个高一致性、高性能的Flutter动态渲染,真的很难么?

相关文章
js性能优化 如何更快速加载你的JavaScript页面
确保代码尽量简洁 不要什么都依赖JavaScript。不要编写重复性的脚本。要把JavaScript当作糖果工具,只是起到美化作用。别给你的网站添加大量的JavaScript代码。只有必要的时候用一下。只有确实能改善用户体验的时候用一下。 ...
2015-11-12
10个强大的纯CSS3动画案例分享
我们的网页外观主要由CSS控制,编写CSS代码可以任意改变我们的网页布局以及网页内容的样式。CSS3的出现,更是可以让网页增添了不少动画元素,让我们的网页变得更加生动有趣,并且更易于交互。本文分享了10个非常炫酷的CSS3动画案例,希望大家...
2015-11-16
v-charts | 饿了么团队开源的基于 Vue 和 ECharts 的图表工具
在使用echarts生成图表时,经常需要做繁琐的数据类型转化、修改复杂的配置项,v-charts的出现正是为了解决这个 痛点。基于Vue2.0和echarts封装的v-charts图表组件,只需要统一提供一种对前后端都友好的数据格式 设置简...
2018-05-24
Node.js 2014这一年发生了什么
Node.js 的 2014 年充满了不幸和争议. 这一年 Noder 们经历了太多的伤心事, 经历了漫长的等待, 经历了沉重的分裂之痛. 也许 Noder 们不想回忆14年 Node.js land 发生的事情, 但正因为痛才更有铭记的价...
2015-11-12
从2014年的发展来展望JS的未来将会如何
&lt;font face=&quot;寰�杞�闆呴粦, Arial, sans-serif &quot;&gt;2014骞达紝杞�浠惰�屼笟鍙戝睍杩呴€燂紝鍚勭�嶈��瑷€灞傚嚭涓嶇┓锛屼互婊¤冻鐢ㄦ埛涓嶆柇鍙樺寲鐨勯渶姹傘€傝繖浜涜��...
2015-11-12
WebSocket断开原因分析,再也不怕为什么又断开了
阅读原文:https://wdd.js.org/websocket-… 1. 把错误打印出来 WebSocket断开的原因有很多,最好在WebSocket断开时,将错误打印出来。 在线demo地址:https://wdd.js.org/we...
2018-04-25
12个你未必知道的CSS小知识
虽然CSS并不是一种很复杂的技术,但就算你是一个使用CSS多年的高手,仍然会有很多CSS用法/属性/属性值你从来没使用过,甚至从来没听说过。 1.CSS的color属性并非只能用于文本显示 对于CSS的color属性,相信所有Web开发人员...
2015-11-12
ajax为什么令人惊异?ajax的优缺点
使用Ajax的最大优点,就是能在不更新整个页面的前提下维护数据。这使得Web应用程序更为迅捷地回应用户动作,并避免了在网络上发送那些没有改变的信息。 Ajax不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。就像DHT...
2015-11-12
HTML5的5个不错的开发工具推荐
HTML5规范终于在今年正式定稿,对于从事多年HTML5开发的人员来说绝对是一个重大新闻。数字天堂董事长,DCloud CEO王安也发表了文章,从开发者和用户两个角度分析了HTML对两个人群的优势。其实,关于HTML5的开发工具,我们以往的...
2015-11-12
JS中的语音合成——Speech Synthesis API
JS中的语音合成——Speech Synthesis API 简介 HTML5中和Web Speech相关的API实际上有两类,一类是“语音识别(Speech Recognition)”,另外一个就是“语音合成(Speech Synthes...
2018-05-17
回到顶部