如何在Flutter上实现高性能的动态模板渲染

Flutter动态模板渲染的性能优化实践

背景

最近小组在尝试使用一套阿里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_parentmatch_content

match_parent:当前节点大小,尽量撑开到父节点大小;
match_content:当前节点大小,尽量缩小到容纳子节点大小;

在Flutter中,并没有match_parentmatch_content的概念。最初我们的想法很简单,在Widget的build方法中,如果属性是match_parent,就不断向上遍历,直到找到一个父节点有确定的宽高值为止;如果是match_content,遍历所有的子节点,获取子节点大小;一旦子节点存在match_content属性,会递归调用下去。

表面上看,做好每个节点的宽高计算的缓存,虽然达不到一次性线性布局,这样的开销也并不是很大。但我们忽略掉了一个很重要的问题:Widget是immutable的,只是包含了视图的配置信息,是非常轻量级的。在Flutter中,Widget会被不断的创建销毁,这会导致布局计算非常的频繁。

要解决这些问题,单单处理Widget是不够的,需要Element以及RenderObject上做更多的处理,这也就是我们为什么要考虑自定义Widget的原因。

认识三棵树

我们通过一个简单的Widget------Opacity来了解一下WidgetElementRenderObject

Widget

在Flutter中,万物皆是Widget,Widget是immutable的,只是包含了视图的配置信息的描述,是非常轻量级的,创建和销毁的开销比较小。

Opacity继承自RenderObjectWidget,其定义了两个比较关键的函数:

dart 复制代码
RenderObjectElement createElement();
RenderObject createRenderObject(BuildContext context);

Element

在SingleChildRenderObjectWidget可以看到创建了SingleChildRenderObjectElement对象。

Element是Widget的抽象,在Widget初始化的时候,调用Widget.createElement创建,Element持有Widget和RenderObject;BuildOwner通过遍历Element Tree,根据是否标记为dirty,构建RenderObject Tree;在整个视图构建过程中,起到了串联Widget和RenderObject的作用。

RenderObject

Opacity的createRenderObject函数创建了RenderOpacity对象,RenderObject真正提供给Engine层渲染所需要的数据,RenderOpacity的Paint方法中找到了真正绘制的地方:

dart 复制代码
void paint(PaintingContext context, Offset offset) {
    if (child != null) {
        ...
        context.pushOpacity(offset, _alpha, super.paint);
    }
}

Flutter在Layout过程中的优化

Flutter采用一次布局的方式,O(N)的线性时间来做布局和绘制。

RelayoutBoundary优化

当一个节点满足如下条件之一,该节点会被标记为RelayoutBoundary,子节点的大小变化不会影响到父节点的布局:

  • parentUsesSize = false:父节点的布局不依赖当前节点的大小
  • sizedByParent = true:当前节点大小由父节点决定
  • constraints.isTight:大小为确定的值,即宽高的最大值等于最小值
  • parent is not RenderObject:如果父节点不是RenderObject,子节点layout变化不需要通知父节点更新

RelayoutBoundary的标记,子节点大小变化,不会通知父节点重新layout,重新paint,从而提高效率。

我们如何自定义Widget

第一个版本的设计

在第一个版本的设计中,我们考虑的比较简单,所有的组件都继承与Object,实现一个build方法,根据DSL转换的nodeData设置Widget的属性:

第二个版本的设计

第二个版本,我们选择自定义Widget、Element以及RenderObject;下面是我们一部分组件的类图。

如何处理match_content

当前节点的宽高设置为match_content,需要先计算子节点的大小,然后再计算当前节点的大小。

如何处理match_parent

如果当前节点的宽高设置为match_parent,尽量扩充到父节点大小;这种情况下,在Constraints向下传递的时候,根据父节点的约束,无需子节点计算,就已经知道自己的大小。

前后方案对比

在第二个版本的设计中,一个Widget渲染,需要怎样一个计算过程呢呢?

经过新方案的优化,长列表滑动的平均帧率从28提升到了50左右。在实际开发过程中,使用像AppUploader这样的iOS开发助手工具可以更方便地测试和验证这些性能优化效果。

更多优化方向

经过一系列的优化之后,页面的卡顿情况终于有所改善,卡顿不再特别明显,但整体帧率仍然达不到Flutter页面的效果。仍然需要对Flutter有更深入的理解,挖掘出过多性能优化的点,进一步做一些更精细化的优化。

展望

目前我们实现了DSL到Widget的映射,这让Flutter动态模板渲染成为了可能。DSL是一种抽象,XML只是其中的一种选择,未来在不断完善性能的同时,还会提升整个方案的抽象,能够支持通用的DSL转换,沉淀一套通用解决方案,更好的通过技术赋能业务。

到Widget的映射,这让Flutter动态模板渲染成为了可能。DSL是一种抽象,XML只是其中的一种选择,未来在不断完善性能的同时,还会提升整个方案的抽象,能够支持通用的DSL转换,沉淀一套通用解决方案,更好的通过技术赋能业务。

对于iOS开发者来说,使用AppUploader这样的工具可以简化应用打包和上传流程,让开发者更专注于性能优化和功能实现。DSL到Widget的转换只是其中一环,从模板的编辑、本地验证、CDN下发、灰度测试、线上监控等整个闭环,仍然有很多需要不断打磨和完善的地方。

相关推荐
默默前行的虫虫5 小时前
解决EMQX WebSocket连接不稳定及优化WS配置提升稳定性?
websocket
那就回到过去6 小时前
MPLS多协议标签交换
网络·网络协议·hcip·mpls·ensp
大方子7 小时前
【PolarCTF】rce1
网络安全·polarctf
枷锁—sha9 小时前
Burp Suite 抓包全流程与 Xray 联动自动挖洞指南
网络·安全·网络安全
聚铭网络9 小时前
聚铭网络再度入选2026年度扬州市网络和数据安全服务资源池单位
网络安全
darkb1rd11 小时前
八、PHP SAPI与运行环境差异
开发语言·网络安全·php·webshell
郝学胜-神的一滴11 小时前
深入浅出:使用Linux系统函数构建高性能TCP服务器
linux·服务器·开发语言·网络·c++·tcp/ip·程序人生
胖咕噜的稞达鸭11 小时前
网络基础:初识TCP/IP协议
网络·网络协议·tcp/ip
全栈工程师修炼指南12 小时前
Nginx | stream content 阶段:UDP 协议四层反向代理浅析与实践
运维·网络·网络协议·nginx·udp