HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

Litho 在动态化方案 MTFlexbox 中的实践

  • 2019-10-05
  • 本文字数:5749 字

    阅读完需:约 19 分钟

Litho在动态化方案MTFlexbox中的实践

1. MTFlexbox

MTFlexbox 是美团内部应用的非常成熟的一种跨平台动态化解决方案,它遵循了 CSS3 中提出的Flexbox规范来抹平多平台的差异。MTFlexbox 适用于重展示、轻交互的业务场景,与现有 HTML、React Native、Weex 等跨平台方案相比,MTFlexbox 具备着性能高、渲染速度快、兼容性高、原生功能支持度高等优势。但其缺点在于不支持复杂的交互逻辑,不适合复杂交互的业务场景。目前,MTFlexbox 已经广泛应用在美团首页、搜索、外卖等重要业务场景。本文主要介绍在 MTFlexbox 中使用 Litho 优化性能的实践经验,更多关于 MTFlexbox 的实践内容,可查阅《MTFlexbox自动化埋点探索》。

1.1 MTFlexbox 的原理

MTFlexbox 首先定义一份跨平台统一的 DSL 布局描述文件,前端通过“所见即所得”的编辑器编辑产生布局,客户端下载布局文件后,根据布局中的描述绑定 JSON 数据,并最终完成视图的渲染。MTFlexbox 框架图如下图所示:



图 1 MTFlexbox 的架构


图中分为五层,分别是:


  • 业务应用层:业务使用 MTFlexbox 的编辑器定义符合 Flexbox 规范的 DSL 文件(XML 模版)。

  • 模版下载:负责 XML 模版下载相关的工作,包括模板缓存、预加载和异常监控等。

  • 模版解析:负责模版解析相关的工作,包括标签节点的预处理、数据绑定、标签节点的缓存复用和数据异常监控等。

  • 视图渲染:负责视图渲染相关的工作,包括把标签结点按照 Flexbox 规范解析成 Native 视图,并完成视图属性的设置、点击曝光事件的处理、视图渲染、异常监控等。

  • 自定义标签扩展:提供支持业务扩展自定义标签的能力。


鉴于本篇博客主要涉及渲染相关的内容,下面将着重介绍 MTFlexbox 从模版解析到渲染的过程。如下图所示,MTFlexbox 首先会把 XML 模版解析成 Java 中的标签树,然后和 JSON 数据绑定结合成一颗具有完整数据信息的节点树。至此,模版解析工作就完成了。解析完成的节点树会交给视图引擎进行 Native 视图树的创建和渲染。



图 2 视图模版从解析到渲染

2. MTFlexbox 在美团动态化实践中面临的挑战

随着 MTFlexbox 在美团内部被广泛使用,我们遇到了两个问题:


  • 复杂视图因层级过深,导致滑动卡顿问题。

  • 生成视图耗时过长,导致滑动卡顿问题。

2.1 问题一:视图层级过深

2.1.1 原因分析

MTFlexbox 使用的是 Flexbox 布局,Flexbox 布局可以理解成 Android LinearLayout 布局的一种扩展。Flexbox 在布局过程中使用到大量的布局嵌套,如果布局酷炫复杂,无疑会出现布局层级过深、视图树遍历耗时、绘制耗时等问题,最终引发滑动卡顿。下图是美团正在使用的一个模版的视图层级情况(布局最深处有 8 层):



图 3 模版布局层级效果

2.1.2 影响

布局层级过深在布局的计算和渲染过程中会导致过多的递归调用,影响视图的绘制效率,引发页面滑动 FPS 下降问题,这会直接影响到用户体验。

2.2 问题二:生成视图耗时过长

2.2.1 原因分析

视图生成耗时原因如下图所示:RecyclerView 在使用 MTFlexbox 布局条目时,需要对条目模版进行下载并解析生成节点树,这样会导致生成视图的过程耗时过长。为了提高视图生成速度,我们增加了复用机制,但是滑动过程中,如果遇到新的布局样式仍然需要重新下载和解析。另外,MTFlexbox 绑定的数据是未经解析的 JSON 字符串,所以也要比正常情况下的数据绑定更耗时一些。 正是上面两个原因,导致了 MTFlexbox 生成视图耗时过长的问题,这也会导致滑动时 FPS 出现突然下降的现象,产生卡顿感。



图 4 视图生成耗时原因分析

2.2.2 影响

由于视图的创建会阻塞主线程,创建视图耗时过长会导致 RecyclerView 列表滑动时卡顿感明显,也严重影响到了用户体验。

3. Litho

3.1 Litho 原理

Litho 是一套声明式 UI 框架,或者说是一个渲染引擎,它主要优化复杂 RecyclerView 列表的滑动性能问题。Litho 实现了视图的细粒度复用、异步计算布局和扁平化视图,可以显著提升滑动性能,减少 RecyclerView 滑动时的内存占用。详细介绍可以参考美团技术团队之前发布的另一篇博客:Litho的使用及原理剖析

3.2 Litho 的优势

通过对 Litho 原理的了解,我们可以看到 Litho 主要针对 RecyclerView 复杂滑动列表做了以下几点优化:


  • 视图的细粒度复用,可以减少一定程度的内存占用。

  • 异步计算布局,把测量和布局放到异步线程进行。

  • 扁平化视图,把复杂的布局拍成极致的扁平效果,优化复杂列表滑动时由布局计算导致的卡顿问题。


扁平化视图刚好可以优化 MTFlexbox 遇到的视图层级过深的问题。异步计算布局虽然不能直接解决 MTFlexbox 生成视图耗时过长问题,但是给问题的解决提供了新的思路——异步提前完成视图创建。而且使用 Litho 还能带来一定程度的内存优化。所以如何将 Litho 应用到 MTFlexbox 中,进而来解决 MTFlexbox 现存的问题,是我们解下来要讨论的重点。

4. Litho + MTFlexbox 是怎么解决上述两个问题的?

4.1 解决问题一:视图层级过深问题

Litho 实现了布局的扁平化,所以最直接的方式就是使用 Litho 来替换 MTFlexbox 现有的视图引擎。视图引擎最主要的作用,是把 XML 文件解析出来的节点树变成 Litho 可以展示的视图,所以视图引擎替换的主要工作是把节点树转换成 Litho 能展示的视图。如下图所示。由于 Litho 使用的是组件化思想,需要先把节点转化成组件,再把组件树设置给 LithoView,而 LithoView 是 Litho 用于兼容原生 View 的容器,它负责把 Litho 和系统视图引擎桥接起来。



图 5 Litho 视图引擎从节点到视图的转换


不过视图引擎的替换并不是一帆风顺的,我们在替换过程中也遇到了 4 个比较大的挑战。


难点一:复用视图无法更新数据问题


问题描述: 完成了节点树到组件树的转化以后,我们发现了一个严重的问题——复用的视图无法应用新的数据。


问题分析: 当数据发生变化后,MTFlexbox 的节点树会对比新旧数据的变更,确定哪些结点需要更新并通知到具体的视图节点,然后更新显示内容(例如:新数据相比旧数据改变了 Text,那么只有 Text 对应的节点会通知对应的视图去更新内容)。Litho 组件的 Prop 属性是不允许更改的,而 Litho 组件中绝大多数属性都是 Prop 属性。


解决方案


方案一:使用 State 属性全局替换所有组件的 Prop 属性。这种方式的优点在于替换方式相对简单直接,缺点是侵入性强,替换工作量巨大且不符合 Litho 的思想(尽可能少的去改变组件的状态)。这种方案不是最优解,我们要降低侵入,简单快捷地实现数据更新,于是就产生了方案二,具体如下图所示。


方案二:封装一套 Updater 组件,用于创建真正展示的组件。Updater 组通过 State 属性监听对应节点的数据变更,当节点数据变化时,可以触发对应节点的更新。



图 6 数据更新问题初版解决方案


但在后来的实践过程中,我们发现 Litho 整个组件树中只要有一个组件有状态更新,便会重新计算整个布局,而每次数据更新少说也会有几十个节点发生变化。频繁的重复计算反而导致性能变得很差。在经过了多种尝试以后,我们找到了最优的解决方案:



图 7 数据更新问题最终解决方案


如上图所示,状态更新控制器负责整个视图所有节点的更新操作。在所有数据都更新完成以后,统一交由状态更新控制器触发一遍组件更新。


难点二:Litho 不支持层叠布局问题


MTFlexbox 并没有完全严格的使用 Flexbox 布局规范,为了简单实现层叠效果,MTFlexbox 自定义了一种新布局规范——Layer 布局。Layer 布局具有以下两个特点:


  • 特点一:Layer 的子视图在 z 轴上依次层叠展示。

  • 特点二:Layer 的子视图默认且只能充满父布局。


原因分析: 由于 Litho 严格遵守 Flexbox 布局规范,所以没有现成的 Layer 组件。


解决方案: 自己实现 Layer 组件,满足第一个特点很容易,Flexbox 本身就支持层叠展示,只需要把子视图设为绝对布局就可以了。但是让子视图默认充满父布局就没有那么简单了,Flexbox 布局中没有任何一个属性可以达到这个效果。在经过了若干次组合多个属性的尝试以后,还是没能找到解决方案。既然 Layer 并不是 Flexbox 布局的规范,那么我们局限在 Flexbox 的束缚下,怕是很难找到完美的解决方案。那么,能不能在 Litho 中绕过 Flexbox 的约束,自己实现 Layer 效果呢?想在 Litho 中突破 Flexbox 布局的束缚,就需要了解 Litho 是如何使用 Flexbox 的。



图 8 Litho 的布局计算原理


如上图,Litho 的 Flexbox 布局是由 Yoga 负责布局计算的。每一个 Litho 组件都会对应一个 Yoga 节点。但 Yoga 的布局计算过程是由根节点去统一触发的,子节点没有办法知道自己对应的 Yoga 节点是何时开始计算,及何时计算结束。这样以来,我们就没有时机去感知到 Layer 组件的布局是否计算完成,也就没有办法在 Layer 组件计算完成后去控制 Layer 子节点的计算。为了解决这个问题,我们做了两件事:


  • 添加布局计算完成的回调,在布局计算完成后由根节点逐层通知子节点计算完成的消息。

  • 拆分 Yoga 节点树,由 Layer 自己来控制子节点的计算。



图 9 Layer 布局的实现原理


如上图所示,把 Layer 组件伪造成叶子节点,不把 Layer 组件的子节点设置给 Yoga,这样一个 Yoga 中的布局树就被 Layer 组件切割开了。当根节点计算完成以后,通知到 Layer 组件,Layer 组件再依次去设置子节点的宽高和位置属性,并触发子节点去完成各自子节点的布局计算。这样就完美地实现了 Layer 的布局效果。


难点三:Litho 图片组件不支持使用网络图片问题


原因分析: Litho 的组件是一个属性的集合,Litho 期望我们在组件创建时便确定了所有属性的值,所以 Litho 不支持网络图的展示。如果要支持从网络下载图片,就意味着图片组件用来展示的内容会发生变化。所以 Litho 自带的图片组件并不支持使用网络图片。


解决方案


方案一:用 State 属性解决网络图片下载带来的展示内容变化问题。我们在实践中发现,State 属性的更新会导致整个布局重新计算,其实替换图片资源不会导致图片组件的大小位置发生变化,根本不需要重新计算布局。为了减少使用 State 属性导致布局计算频繁的问题,就摒弃了这种方案。


方案二:Litho 官方额外提供的异步下载图片组件FrescoImage中使用的是图片代理方式。FrescoImage 使用 DraweeDrawable 来绘制视图,而 DraweeDrawable 实际上并不具备图片渲染的能力,只是在内部保存了一个真正的 Drawable 来负责渲染。所以,DraweeDrawable 本质上是对真正要展示的图片做了一层代理,当从网络上下载下来真正要展示的图片后,只需要通过替换代理图片就可以完成视图的更新。美团下载图片使用的是 Glide,只需要按照这个思路实现自己的 GlideDrawable 就好了。


难点四:自定义标签扩展的接口不兼容问题


MTFlexbox 支持自定义标签的扩展,所以我们在完成基本视图标签的 Litho 实现以后,还需要支持自定义 Tag 的扩展,才算完成视图引擎的替换工作。


原因分析:MTFlexbox 在设计自定义标签接口时,只提供了允许使用 View 完成视图扩展的接口,但是 Litho 实现的视图引擎是使用组件作为视图单元和 MTFlexbox 对接的,所以接口不能兼容。


解决方案


方案一:重新提供使用 Litho 组件完成视图扩展的接口。其缺点是,需要 MTFlexbox 的使用方重新实现已经支持了的自定义标签,工作量较大,所以这种方案被抛弃了。


方案二:Litho 中使用业务方已经扩展好的 View。其优点是使用方对视图引擎的替换无感知。那么,怎样才能在 Litho 中使用业务方已经扩展好的 View 呢?可以先看下面这张图。



图 10 Litho 对 View 功能的拆分


我们可以简单的理解成 Litho 对 Android 的 View 做了一个功能拆分,把属性和布局计算的能力放在了组件里面,每一种组件对应一个绘制单元来专门负责绘制。那么对于使用方扩展的标签,我们可以定义一个通用组件来统一承接。在挂载绘制单元时,再去调用使用方扩展的视图去绘制。


优化效果


至此,视图引擎的替换就完成了,整个视图引擎的替换做到了使用方无感知。完美解决了 MTFlexbox 视图层级深的问题,顺带还优化了部分性能。下面是布局层级优化效果的对比,可以看到相同样式下,使用 Litho 引擎实现的视图比使用 MTFlexbox 原生引擎的视图层级要浅很多。



除此之外,还有我们的内存优化成果。下图是美团首页使用 MTFlexbox 时,内存占用随滑动页数(一页为 20 条数据)增加而变化的趋势图。可以看到,使用 Litho 引擎实现的 MTFlexbox 比使用原生引擎的 MTFlexbox 在内存占用上能有 30M 以上的优化。


4.2 解决问题二:生成视图耗时过长

上文提到导致生成视图耗时过长的有两个原因:


a. MTFlexbox 对布局模版的下载和解析耗时。


b. MTFlexbox 绑定时解析数据的耗时。


上文“自定义标签扩展的接口不兼容问题”中介绍过 Litho 的组件能够独立完成布局计算。另外,Litho 组件是轻量级的,所以我们直接把 Litho 组件作为 RecyclerView 适配器的数据源。这样就需要在数据解析时提前完成组件的创建,而组件的创建需要用到 MTFlexbox 的整个解析过程,也就是说,我们把 MTFlexbox 导致视图生成耗时过长的过程提前在数据层异步完成了。这样就不需要等到视图要展示时再去解析,从而规避了视图生成耗时过长的问题。具体的原理,可以参见Litho的使用及原理剖析一文中的 3.2 节“异步布局”。



如上图所示,在异步线程中提前完成 MTFlexbox 布局到 Litho 组件的转换。当视图真正要展示时,只需要把组件设置给 LithoView 就可以了。


优化效果


使用 Litho 引擎实现的滑动列表,在连续滑动过程中不会出现 FPS 波动问题,而使用 MTFlexbox 原生引擎实现的滑动列表则波动明显。(数据采集自魅蓝 2 手机,中低端手机优化效果明显。)


5. 总结

经过一段时间的实践,Litho + MTFlexbox 给美团 App 在性能指标上带来了较大的提升。但是还有很多问题待完善,我们后续还会针对以下几点进一步提升效果:


  • 利用 Litho 组件属性不可变的特点,将提前异步布局进一步扩展为提前渲染出位图,在绘制时直接展示位图,可以进一步提升绘制效率。

  • 优化 RecyclerView 相关的 API,降低侵入性。

  • 解决有点击事件、埋点事件等属性的视图需要降级成 View 才能使功能生效的问题,进一步优化视图层级。

6.参考资料

Litho官网


Flexbox规范


作者介绍


少宽腾飞叶梓,美团终端业务研发团队开发工程师。


本文转载自公众号美团技术团队(ID:meituantech)


原文链接


https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651750679&idx=2&sn=b9b3e95b9c35869081b5f76d542dc7ff&chksm=bd12585a8a65d14c738fbb83cacaebf53fb735ae1a745b15dfca2b5fe386af60ca06233a4af4&scene=27#wechat_redirect


2019-10-05 08:002017

评论

发布
暂无评论
发现更多内容

MyBatis是如何初始化的

华为云开发者联盟

Java 开发 华为云 12 月 PK 榜

技术分享 | 测试的本质是什么?

霍格沃兹测试开发学社

数据人PK也无人,为什么业务部门的数据需求都是急活?

雨果

数据开发 数据工程师 数据服务

校招面试真题 | 你的期望薪资是多少?为什么?

测试人

网络ping不通,试试这8招

华为云开发者联盟

开发 网络 服务器 华为云 12 月 PK 榜

TypeScript 前端工程最佳实践

京东科技开发者

typescript 前端 前端开发 编程语言】

车载LED显示屏的4大性能指标

Dylan

LED显示屏 户外LED显示屏 led显示屏厂家

如何在云原生环境中实现安全左移?

SEAL安全

云原生 安全 DevSecOps 12 月 PK 榜

校招面试真题 | 你的期望薪资是多少?为什么?

霍格沃兹测试开发学社

远程灵活办公,就用华为云桌面

科技说

什么是数据管理?看完这篇你一定有收获

雨果

数据管理

大道至简,自治为王 | 2022年12月《中国数据库行业分析报告》精彩抢先看

墨天轮

数据库 Serverless 云数据库 国产数据库 polarDB

跨平台应用开发进阶(三十六) :uniapp使用uni.request请求报错{“errMsg“:“request:fail abort statusCode:-1“}的解决办法

No Silver Bullet

uni-app 12月月更 跨平台应用开发 statusCode:-1“ request:fail abort

探索科创服务升级之路,星创科服“贴身陪伴”硬科技冠军企业成长

硬科技星球

数据中台选型前必读(七):解读数据服务的四大关键技术

雨果

数据中台 DaaS数据即服务

企业大数据价值最大化的关键因素

元年技术洞察

大数据 数据中台 数字化转型

跨平台应用开发进阶(三十七)uni-app前端监控方案 Sentry 探究

No Silver Bullet

uni-app sentry 12月月更 前端监控方案

weidl x DeepRec:热门微博推荐框架性能提升实战

阿里云大数据AI技术

性能优化 AI技术 推荐引擎 12 月 PK 榜

学生管理系统架构文档

闲人Eric

架构实战营

论文复现丨基于ModelArts进行图像风格化绘画

华为云开发者联盟

人工智能 华为云 12 月 PK 榜

带你读AI论文丨针对文字识别的多模态半监督方法

华为云开发者联盟

人工智能 华为云 文字识别 12 月 PK 榜

跨平台应用开发进阶(三十四) :uni-app 应用 Universal Link 实现 iOS 微信分享

No Silver Bullet

uni-app universal link 跨平台应用 12月月更 iOS 微信分享

这个团队敢闯、会创,北京交通大学团队结合昇思MindSpore技术助力打造“智慧安全交通”

Geek_2d6073

云上安全办公,就用华为云桌面

科技说

【kafka运维】Leader重新选举运维脚本

石臻臻的杂货铺

kafka 运维

图算法、图数据库在风控场景的应用

NebulaGraph

图数据库 风控

2022中国产业数字化发展成熟度行业指数分析—— 重视差异,结合自身要素禀赋,推进产业精细化治理

易观分析

产业 产业数字化

低代码多分支协同开发的建设与实践

阿里巴巴终端技术

前端 低代码

计算机科学通识-01-电子计算机发展史

邱比特讲编程

计算机基础 计算机 计算机教育

如何通过Java提取PDF中的图片

Geek_249eec

Java PDF 图片

头像类NFT的未来,实际价值在哪里?

博文视点Broadview

Litho在动态化方案MTFlexbox中的实践_语言 & 开发_叶梓_InfoQ精选文章