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

栏目: IT技术 · 发布时间: 4年前

内容简介:出品|阿里巴巴新零售淘系技术部

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

作者| 章志坚(皓黯)  

出品|阿里巴巴新零售淘系技术部

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

思路

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

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

分析了这些 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 的情况):

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

我们可以基于上面这个表格,计算出每个 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 来表达。

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

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

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

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;


@override

boolget sizedByParent {

return nodeData.widthMeasureMode == DXMeasureMode.DX_EXACTLY &&

nodeData.heightMeasureMode == DXMeasureMode.DX_EXACTLY;

}


......

}

▐  如何实现performResize

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

abstractclassDXRenderBoxextendsRenderBox{

......


@override

void 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 具体有多大,就是通过它来确定的。

abstractclassDXSingleChildLayoutRenderextendsDXRenderBox

withRenderObjectWithChildMixin<RenderBox> {


@override

void 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{

@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部分

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的问题?

我们先来看一个例子:

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

上图的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 为例子来说明该如何实现这些方法。

@override

double 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 动态模板方案带来了更多的可能性。

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

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

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

后续展望

在渲染架构升级之后,我们彻底解决了之前遇到的 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

We are hiring

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

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

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

好文!必须点赞


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Art and Science of Java

The Art and Science of Java

Eric Roberts / Addison-Wesley / 2007-3-1 / USD 121.60

In The Art and Science of Java, Stanford professor and well-known leader in CS Education Eric Roberts emphasizes the student-friendly exposition that led to the success of The Art and Science of C. By......一起来看看 《The Art and Science of Java》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

随机密码生成器
随机密码生成器

多种字符组合密码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换