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

Flutter动态模板渲染架构升级

​ 最近小组在尝试使用集团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上的一颗子树,这棵子树主要由三部分组成。

  1. Decoration层:Decoration层用于支持背景色、边框、圆角、触摸事件等,这些我们可以通过组合方式实现。
  2. Render层:Render层用于表达Node在转化后的布局规则与尺寸大小。
  3. 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方法做了简化,代码如下:

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

​ 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;@overridebool get sizedByParent {return nodeData.widthMeasureMode == DXMeasureMode.DX_EXACTLY &&nodeData.heightMeasureMode == DXMeasureMode.DX_EXACTLY;}......
}

如何实现performResize

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

abstract class DXSingleChildLayoutRender extends DXRenderBoxwith RenderObjectWithChildMixin<RenderBox> {@overridevoid 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

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

class DXFrameLayoutRender extends DXMultiChildLayoutRender {  @overridevoid performLayout() {BoxConstraints childrenBoxConstraints = computeChildBoxConstraints();double maxWidth = 0.0;double maxHeight = 0.0;//layout childrenvisitDXChildren((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 offsetsvisitDXChildren((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的宽/高为match_content,而其children的宽/高均为match_parent。这种情况在Android下会对同一个child进行"两次measure",那么在Flutter下,我们该如何实现呢?

Flutter如何解决"两次Measure"的问题

我们先来看一个例子:

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

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

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

  1. 先在performResize中计算自身size,再通过child.layout确定children sizes
  2. 先通过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);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

原文链接
本文为云栖社区原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/517528.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据科学产业中哪些架构最热门?本文为你盘点了 5 款!

作者 | Sai Krishna译者 | 火火酱&#xff0c;责编 | Carol封图 | CSDN 付费下载自视觉中国地球上的数据量每分每秒都在增加&#xff0c;海量的数据源源不断地从四面八方涌入各种机构组织&#xff0c;而这些数据最终或许会成为能够指引我们做出战略决策的宝贵财富。这就是数据科…

面向云原生的混沌工程工具-ChaosBlade

作者 | 肖长军&#xff08;穹谷&#xff09;阿里云智能事业群技术专家 导读&#xff1a;随着云原生系统的演进&#xff0c;如何保障系统的稳定性受到很大的挑战&#xff0c;混沌工程通过反脆弱思想&#xff0c;对系统注入故障&#xff0c;提前发现系统问题&#xff0c;提升系…

centOS docker容器的安装

文章目录一、docker安装步骤1.卸载旧版本2.安装依赖3.添加镜像源4.查看仓库中的所有docker版本5.安装docker6.启动docker7.设置开机自启动docker8.查看docker是否安装成功9.卸载docker一、docker安装步骤 提示&#xff1a;安装之前可以使用命令&#xff1a;yum check-update检查…

泡着枸杞写bug的三流程序员凭什么逆袭到一线大厂?

大多数互联网的从业者都有一个梦想&#xff1a;进大厂。因为不仅可以享受较好的福利待遇&#xff0c;也能与更优秀的人一起共事&#xff0c;获得更专业、更快速的成长。最近经常有朋友提及想要入门编程学习&#xff0c;该如何学习&#xff1f;关于编程学习&#xff0c;各种语言…

哪种人是软件设计中的稀缺型人才?

阿里妹导读&#xff1a;好的系统架构离不开好的接口设计&#xff0c;因此&#xff0c;真正懂接口设计的人往往是软件设计队伍中的稀缺型人才。 为什么在接口制定标准中说&#xff1a;一流的企业做标准&#xff0c;二流的企业做品牌&#xff0c;三流的企业做产品&#xff1f;依赖…

快速Ubuntu的配置(以ubuntu 20.04桌面版为例)

文章目录一、&#x1f496;更换镜像源二、&#x1f496;安装VMware Tools工具三、&#x1f496;安装Google浏览器四、&#x1f496;安装搜狗输入法五、&#x1f496;安装C/C开发工具Clion&#x1f340;1.安装编译环境&#x1f340;2.安装开发环境一、&#x1f496;更换镜像源 …

阿里工程师太凶残了,竟把服务器泡在“水里”!

万众瞩目的第11个双11要来了&#xff01;这次天猫说要帮大家省500亿&#xff0c;身边加购物车、组队盖楼的同学数不胜数&#xff0c;热闹非凡。 但在阿里&#xff0c;有一位低调的“高冷男”&#xff0c;丝毫不为红红火火的双11所动&#xff0c;依然稳如磐石、淡定自若。 他的…

“我今年 31 岁,工作 7 年,明年退休...”

凌晨 1:30 的北京&#xff0c;商务楼静静地伫立着&#xff0c;街道上的车水马龙&#xff0c;慢慢地停歇了&#xff0c;只有他的工作台灯还没有休息&#xff0c;台灯忽明忽暗地坚持着。凌晨 2:00 的闹钟忍不住提醒他&#xff0c;电脑也偷偷跳出窗口&#xff1a;“主人&#xff0…

据说这是双11前互联网人的一天~

双11前&#xff0c;据说互联网人的一天是这样度过.... 6段故事&#xff0c;6个黑话关键词&#xff0c;生动刻画了双11前互联网人的一天&#xff01; 上午9点&#xff0c;运营找老板汇报双11方案。 运营说&#xff1a;老板&#xff0c;本次营销方案你看需要一些预算资源支持&a…

快速入门docker容器

文章目录&#x1f332;1.拉取一个镜像&#x1f332;2. 在docker里启动一个镜像&#x1f332;3.查看所有的镜像列表&#x1f332;4.删除镜像&#x1f332;5.删除全部image镜像&#x1f332;6.运行tomcat&#x1f332;7.删除所有未运行的容器&#x1f332;8.以守护态运行容器&…

不会玩游戏的程序员不是好作家,《深入理解Java虚拟机》周志明来了!

嘉宾&#xff1a;周志明、杨福川采访、撰文&#xff1a;Satoh_AI这次采访起源来自于我和豆瓣的一位读者有同样的好奇心&#xff0c;为什么网上搜不到周志明老师的更多信息&#xff1f;为什么“80后玩家”可以把本本书都维持在9.0分左右&#xff1f;他的“社恐”到底有多严重?所…

当手机淘宝遇见折叠屏,让购物更随心

华为 5G 新品发布会上&#xff0c;Mate X 正式亮相&#xff0c;淘宝也作为重点展示应用出现在发布会的 PPT 上&#xff0c;同时也成为折叠屏生态联盟应用矩阵的第一位。 现场华为折叠屏上的淘宝多任务演示 伴随手淘技术团队对华为折叠屏适配工作的展开。半年前还只是概念方案的…

SpringBoot2.x 整合 Ueditor

文章目录一、基础准备1. 创建项目并引入依赖2. 下载Ueditor源码3. Java代码整合4. 静态文件整合二、静态页面控制层2.1. index.html2.2. demo1.html2.3. demo2.html2.4. demo3.html2.5. Controller三、配置调整3.1. 图片大小3.2. 修改ueditor.config.js3.3. 修改config.json文件…

无线路由攻击和WiFi密码破解实战[渗透技术]

文章目录一、准备阶段二、攻击阶段1.停止&#x1f6d1;网络管理员2.开启网卡监听模式3. 捕获数据包4.获取数据包5.注入数据包&#xff08;DeAuth洪水攻击&#xff09;5.WiFi密码破解一、准备阶段 攻击主机&#xff1a;kali Linux攻击工具&#xff1a;aircrack-ng、airodump-ng…

支付宝双11狂欢幕后的女程序员:服务全球12亿人,每天和不法分子打攻防战

再过3天&#xff0c;全球最大的购物狂欢节就开始了。 在这个睡不着的午夜&#xff0c;无数男男女女会在闪烁的屏幕前滑屏、抢购、享受秒级付款带来的快感。整个过程大脑分泌的多巴胺&#xff0c;又驱使他们以更快的速度重新填满购物车。 2018年天猫双11成交额2135亿元&#x…

技术直播:程序员副业的修炼指南!(限免报名)

面试造飞机&#xff0c;上班拧螺丝&#xff0c;每天想辞职&#xff0c;但无奈副业还“大器晚成”的样子&#xff01;那可能是你还没有选对副业&#xff01;滴滴 ~福利卡&#xff01;&#xff01;&#xff01;CSDN学院邀请汤小洋老师开设技术直播课《程序员副业之路-三大终极秘籍…

轻松解决Android gradle太慢问题

夫陶公清风千古&#xff0c;余又何人&#xff0c;敢称庶几 一、解决方案 从网上下载对应版本的gradle,然后把gradle压缩包复制到C:\Users\liuxin\.gradle\wrapper\dists\gradle-6.5-all\2oz4ud9k3tuxjg84bbf55q0tn目录下&#xff0c;重新build工程就搞定了。 安卓开发者平台官…

持续交付体系在高德的实践历程

1. 前序 对于工程团队来说&#xff0c;构建一套具有可持续性的、多方面质量保证的交付体系建设&#xff0c;能够为业务价值的快速交付搭建起高速公路&#xff0c;也能为交付过程中的质量起到保驾护航的作用。本文为大家介绍持续交付体系在高德的演进与落地。 2. 持续交付 正…

RuoYi-Cloud 部署篇_01(windows环境 mysql +nginx版本)

文章目录一、基础准备1. 技术选型2. 源码克隆3. 安装依赖4. 将 RuoYi-Cloud 项目导入到 IDEA5. 安装启动Mysql6. 安装启动Redis7. 创建数据库&#xff0c;执行 SQL脚本文件二、安装与配置 nacos2.1. 下载nacos2.2. 安装 nacos2.3. nacos持久化配置2.4. 执行脚本文件2.5. nacos连…

趣头条基于 Flink 的实时平台建设实践

本文由趣头条实时平台负责人席建刚分享趣头条实时平台的建设&#xff0c;整理者叶里君。文章将从平台的架构、Flink 现状&#xff0c;Flink 应用以及未来计划四部分分享。 一&#xff0e;平台架构 1.Flink 应用时间线 首先是平台的架构&#xff0c;2018 年 3 月之前基本都是基…