如何在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下发、灰度测试、线上监控等整个闭环,仍然有很多需要不断打磨和完善的地方。

相关推荐
2501_9160074718 分钟前
绕过 Xcode?使用 Appuploader和主流工具实现 iOS 上架自动化
websocket·网络协议·tcp/ip·http·网络安全·https·udp
2501_9160137419 分钟前
使用 Windows 完成 iOS 应用上架:Appuploader对比其他证书与上传方案
websocket·网络协议·tcp/ip·http·网络安全·https·udp
济宁雪人1 小时前
HTTP协议
网络·网络协议·http
S侯1 小时前
💻🚀一行代码简化请求!⚡Alova策略库打造🔄流畅体验!!
前端·https
网硕互联的小客服1 小时前
如何防止服务器被用于僵尸网络(Botnet)攻击 ?
网络·网络安全·ddos
emo了小猫2 小时前
HTTP连接管理——短连接,长连接,HTTP 流水线
网络·网络协议·http
余辉zmh2 小时前
【Linux网络篇】:从HTTP到HTTPS协议---加密原理升级与安全机制的全面解析
linux·网络·http
muyouking113 小时前
用 n8n 提取静态网页内容:从 HTTP Request 到 HTML 节点全解析
网络协议·http·html
程序员的世界你不懂11 小时前
(9)-Fiddler抓包-Fiddler如何设置捕获Https会话
前端·https·fiddler
浩浩测试一下14 小时前
Authpf(OpenBSD)认证防火墙到ssh连接到SSH端口转发技术栈 与渗透网络安全的关联 (RED Team Technique )
网络·网络协议·tcp/ip·安全·网络安全·php