MTFlexbox是美团内部应用的很是成熟的一种跨平台动态化解决方案,它遵循了CSS3中提出的Flexbox规范来抹平多平台的差别。MTFlexbox适用于重展现、轻交互的业务场景,与现有HTML、React Native、Weex等跨平台方案相比,MTFlexbox具有着性能高、渲染速度快、兼容性高、原生功能支持度高等优点。但其缺点在于不支持复杂的交互逻辑,不适合复杂交互的业务场景。目前,MTFlexbox已经普遍应用在美团首页、搜索、外卖等重要业务场景。本文主要介绍在MTFlexbox中使用Litho优化性能的实践经验。css
MTFlexbox首先定义一份跨平台统一的DSL布局描述文件,前端经过“所见即所得”的编辑器编辑产生布局,客户端下载布局文件后,根据布局中的描述绑定JSON数据,并最终完成视图的渲染。MTFlexbox框架图以下图所示:html
图中分为五层,分别是:前端
鉴于本篇博客主要涉及渲染相关的内容,下面将着重介绍MTFlexbox从模版解析到渲染的过程。以下图所示,MTFlexbox首先会把XML模版解析成Java中的标签树,而后和JSON数据绑定结合成一颗具备完整数据信息的节点树。至此,模版解析工做就完成了。解析完成的节点树会交给视图引擎进行Native视图树的建立和渲染。git
随着MTFlexbox在美团内部被普遍使用,咱们遇到了两个问题:github
MTFlexbox使用的是Flexbox布局,Flexbox布局能够理解成Android LinearLayout布局的一种扩展。Flexbox在布局过程当中使用到大量的布局嵌套,若是布局酷炫复杂,无疑会出现布局层级过深、视图树遍历耗时、绘制耗时等问题,最终引起滑动卡顿。下图是美团正在使用的一个模版的视图层级状况(布局最深处有8层):后端
布局层级过深在布局的计算和渲染过程当中会致使过多的递归调用,影响视图的绘制效率,引起页面滑动FPS降低问题,这会直接影响到用户体验。缓存
视图生成耗时缘由以下图所示:RecyclerView在使用MTFlexbox布局条目时,须要对条目模版进行下载并解析生成节点树,这样会致使生成视图的过程耗时过长。为了提升视图生成速度,咱们增长了复用机制,可是滑动过程当中,若是遇到新的布局样式仍然须要从新下载和解析。另外,MTFlexbox绑定的数据是未经解析的JSON字符串,因此也要比正常状况下的数据绑定更耗时一些。 正是上面两个缘由,致使了MTFlexbox生成视图耗时过长的问题,这也会致使滑动时FPS出现忽然降低的现象,产生卡顿感。网络
因为视图的建立会阻塞主线程,建立视图耗时过长会致使RecyclerView列表滑动时卡顿感明显,也严重影响到了用户体验。架构
Litho是一套声明式UI框架,或者说是一个渲染引擎,它主要优化复杂RecyclerView列表的滑动性能问题。Litho实现了视图的细粒度复用、异步计算布局和扁平化视图,能够显著提高滑动性能,减小RecyclerView滑动时的内存占用。详细介绍能够参考美团技术团队以前发布的另外一篇博客:Litho的使用及原理剖析。框架
经过对Litho原理的了解,咱们能够看到Litho主要针对RecyclerView复杂滑动列表作了如下几点优化:
扁平化视图恰好能够优化MTFlexbox遇到的视图层级过深的问题。异步计算布局虽然不能直接解决MTFlexbox生成视图耗时过长问题,可是给问题的解决提供了新的思路——异步提早完成视图建立。并且使用Litho还能带来必定程度的内存优化。因此如何将Litho应用到MTFlexbox中,进而来解决MTFlexbox现存的问题,是咱们解下来要讨论的重点。
Litho实现了布局的扁平化,因此最直接的方式就是使用Litho来替换MTFlexbox现有的视图引擎。视图引擎最主要的做用,是把XML文件解析出来的节点树变成Litho能够展现的视图,因此视图引擎替换的主要工做是把节点树转换成Litho能展现的视图。以下图所示。因为Litho使用的是组件化思想,须要先把节点转化成组件,再把组件树设置给LithoView,而LithoView是Litho用于兼容原生View的容器,它负责把Litho和系统视图引擎桥接起来。
不过视图引擎的替换并非一路顺风的,咱们在替换过程当中也遇到了4个比较大的挑战。
难点一:复用视图没法更新数据问题
问题描述:完成了节点树到组件树的转化之后,咱们发现了一个严重的问题——复用的视图没法应用新的数据。
问题分析:当数据发生变化后,MTFlexbox的节点树会对比新旧数据的变动,肯定哪些结点须要更新并通知到具体的视图节点,而后更新显示内容(例如:新数据相比旧数据改变了Text,那么只有Text对应的节点会通知对应的视图去更新内容)。Litho组件的Prop属性是不容许更改的,而Litho组件中绝大多数属性都是Prop属性。
解决方案
方案一:使用State属性全局替换全部组件的Prop属性。这种方式的优势在于替换方式相对简单直接,缺点是侵入性强,替换工做量巨大且不符合Litho的思想(尽量少的去改变组件的状态)。这种方案不是最优解,咱们要下降侵入,简单快捷地实现数据更新,因而就产生了方案二,具体以下图所示。
方案二:封装一套Updater组件,用于建立真正展现的组件。Updater组经过State属性监听对应节点的数据变动,当节点数据变化时,能够触发对应节点的更新。
但在后来的实践过程当中,咱们发现Litho整个组件树中只要有一个组件有状态更新,便会从新计算整个布局,而每次数据更新少说也会有几十个节点发生变化。频繁的重复计算反而致使性能变得不好。在通过了多种尝试之后,咱们找到了最优的解决方案:
如上图所示,状态更新控制器负责整个视图全部节点的更新操做。在全部数据都更新完成之后,统一交由状态更新控制器触发一遍组件更新。
难点二:Litho不支持层叠布局问题
MTFlexbox并无彻底严格的使用Flexbox布局规范,为了简单实现层叠效果,MTFlexbox自定义了一种新布局规范——Layer布局。Layer布局具备如下两个特色:
缘由分析: 因为Litho严格遵照Flexbox布局规范,因此没有现成的Layer组件。
解决方案: 本身实现Layer组件,知足第一个特色很容易,Flexbox自己就支持层叠展现,只须要把子视图设为绝对布局就能够了。可是让子视图默认充满父布局就没有那么简单了,Flexbox布局中没有任何一个属性能够达到这个效果。在通过了若干次组合多个属性的尝试之后,仍是没能找到解决方案。既然Layer并非Flexbox布局的规范,那么咱们局限在Flexbox的束缚下,怕是很难找到完美的解决方案。那么,能不能在Litho中绕过Flexbox的约束,本身实现Layer效果呢?想在Litho中突破Flexbox布局的束缚,就须要了解Litho是如何使用Flexbox的。
如上图,Litho的Flexbox布局是由Yoga负责布局计算的。每个Litho组件都会对应一个Yoga节点。但Yoga的布局计算过程是由根节点去统一触发的,子节点没有办法知道本身对应的Yoga节点是什么时候开始计算,及什么时候计算结束。这样以来,咱们就没有时机去感知到Layer组件的布局是否计算完成,也就没有办法在Layer组件计算完成后去控制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呢?能够先看下面这张图。
咱们能够简单的理解成Litho对Android的View作了一个功能拆分,把属性和布局计算的能力放在了组件里面,每一种组件对应一个绘制单元来专门负责绘制。那么对于使用方扩展的标签,咱们能够定义一个通用组件来统一承接。在挂载绘制单元时,再去调用使用方扩展的视图去绘制。
优化效果
至此,视图引擎的替换就完成了,整个视图引擎的替换作到了使用方无感知。完美解决了MTFlexbox视图层级深的问题,顺带还优化了部分性能。下面是布局层级优化效果的对比,能够看到相一样式下,使用Litho引擎实现的视图比使用MTFlexbox原生引擎的视图层级要浅不少。
除此以外,还有咱们的内存优化成果。下图是美团首页使用MTFlexbox时,内存占用随滑动页数(一页为20条数据)增长而变化的趋势图。能够看到,使用Litho引擎实现的MTFlexbox比使用原生引擎的MTFlexbox在内存占用上能有30M以上的优化。
上文提到致使生成视图耗时过长的有两个缘由:
(1) MTFlexbox对布局模版的下载和解析耗时。
(2) MTFlexbox绑定时解析数据的耗时。
上文“自定义标签扩展的接口不兼容问题”中介绍过Litho的组件可以独立完成布局计算。另外,Litho组件是轻量级的,因此咱们直接把Litho组件做为RecyclerView适配器的数据源。这样就须要在数据解析时提早完成组件的建立,而组件的建立须要用到MTFlexbox的整个解析过程,也就是说,咱们把MTFlexbox致使视图生成耗时过长的过程提早在数据层异步完成了。这样就不须要等到视图要展现时再去解析,从而规避了视图生成耗时过长的问题。具体的原理,能够参见Litho的使用及原理剖析一文中的3.2节“异步布局”。
如上图所示,在异步线程中提早完成MTFlexbox布局到Litho组件的转换。当视图真正要展现时,只须要把组件设置给LithoView就能够了。
优化效果
使用Litho引擎实现的滑动列表,在连续滑动过程当中不会出现FPS波动问题,而使用MTFlexbox原生引擎实现的滑动列表则波动明显。(数据采集自魅蓝2手机,中低端手机优化效果明显)
通过一段时间的实践,Litho + MTFlexbox给美团App在性能指标上带来了较大的提高。可是还有不少问题待完善,咱们后续还会针对如下几点进一步提高效果:
少宽、腾飞、叶梓,美团终端业务研发团队开发工程师。
美团终端业务研发团队的职责是保障平台业务高效、稳定迭代的同时,持续优化用户体验和研发效率。团队负责的业务主要包括美团首页、美团搜索等千万级DAU高频业务以及分享、帐号、音/视频等基础业务,支撑和对接外卖、酒店等30多个业务方。
团队经过动态化能力建设,加快业务上线速度,帮助产品团队快速验证业务选型,作出业务决策;经过架构/服务标准化体系建设,提高先后端以及平台与业务线的沟通、合做效率;业务监控和体验优化,有效保障核心业务服务成功率的同时,提高用户使用美团App过程当中的稳定性和流畅性。团队开发技术栈包括Android、iOS、ReactNative、Flexbox等。
美团终端业务研发团队现诚聘Android、iOS工程师,欢迎有兴趣的同窗投简历至:tech@meituan.com(注明:美团终端业务研发团队)。