近些年,移动端动态化技术可谓是“百花齐放”,其中的渲染性能也是动态化技术一直在探索、研究的课题。美团的开源框架 Graver 也为解决动态化框架的渲染性能问题提供了一种新思路:关于布局,我们可以采用“画控件”方案替代传统的“拼控件”方式。本文尝试给出一些探索思考与实践经验的分享。

前言

动态化技术指的是不依赖程序安装包,就能进行动态实时更新页面的技术。特别是对于电商、社交等需要快速迭代、实时调整的强运营类业务来说,动态化具有非常重要意义。它的优势主要表现为:提高人效、缩短迭代试错周期、解决版本长尾问题、减少包大小等等。

从2018年开始,移动端设备的增长红利不再,整个生态增长趋势开始由高走低,与之对应的开发生态在 Native 技术方向也逐渐开始进入低迷阶段,大方向在向跨平台演进,方案上已经是“百花齐放”。现有的客户端动态化技术主要可以划分为以基于 Webview 的 Web 页面动态化加载、本地内置多个模板支持动态切换、支持动态 DSL 的布局引擎以及基于虚拟机等四类。

动态化方案的渲染引擎多数是基于原生 UI 控件搭建动态化页面。基于 Webview 的 Web 页面动态化,实质是基于浏览器运行网页,页面绘制效率、运行效率相对较低一些。而后三种解决方案,分别通过建立映射表、布局引擎、虚拟机与客户端渲染引擎通讯及调用关系,渲染引擎则都是基于原生 UI 控件搭建动态化页面。由于操作系统提供的 UI 控件布局/绘制仅支持主线程访问,大量原生 UI 控件操作导致 CPU/GPU 负担过重,所以在构建复杂的动态化页面上存在效率和性能瓶颈。因此,渲染性能是动态化技术一直在探索、研究的课题。本文尝试给出一些探索思考与实践经验的分享。

动态布局框架

如前言部分的图1所示,MTFlexbox 是美团点评自研的一款跨平台动态布局框架,它遵循了 CSS3 中提出的 Flexbox 规范来抹平多端差异。美团 App 首页、搜索结果页等业务有一个共同点,就是面向的业务方比较多,承载了流量输送变现的能力。在视图层面呈现轻交互、重展示的特征。频繁变动 UI,快速上线是一个刚需,MTFlexbox 正是满足了这样一个刚需。

由于本文侧重对 MTFlexbox 的渲染性能优化,故仅对 MTFlexbox 做概括介绍。MTFlexbox 首先定义了一份跨平台统一的 DSL 布局描述文件,前端通过编辑器编辑产生布局文件并上传到云端,客户端下载布局文件然后根据布局中的描述信息绑定业务数据,最后基于原生 UI 控件搭建视图并渲染展示。MTFlexbox 的工作原理如下图所示:

业务痛点

然而,随着业务的迭代演变,美团 App 首页、搜索结果页等业务视图卡片样式越来越多,展示也越来越复杂。样式种类多意味着视图复用率低,极端场景下甚至无法进行复用。展示复杂,同时也意味着控件数量多、布局复杂、层级深。如果大量复杂操作都发生在主线程,难免造成渲染卡顿等用户体验方面的问题。

针对上述问题,外卖终端用户研发组、美团终端技术研发组、美团终端业务研发组合作共赢,三方协调资源成立了跨部门、跨事业部的虚拟专项联合项目组,三方精诚合作,在技术上不断追求卓越,力求同时保证稳定性、动态化和高性能。

思路分析

动态布局框架 MTFlexbox 通过系统 UIKit 搭建视图并渲染展示,其测量、布局、绘制过程均发生在主线程。而作为一款 iOS 端高效的 UI 异步渲染框架 Graver,其布局计算、渲染过程完全异步化,整个过程结束后才通知 UI 线程进行展示。这给我们解决动态化框架的渲染性能问题打开了新思路:关于布局,我们可以采用“画控件”方案替代传统的“拼控件”方式。

Graver 已经在美团 App 的外卖频道、独立外卖 App 核心业务场景的多个业务中经历了一年多的实践检验。良好的稳定性和出色的渲染性能,也得到了美团外卖内部技术团队的认可和肯定。关于 Graver 更多的内容这里不再赘述,详细介绍请参考另一篇技术博客:《美团开源 Graver 框架:用“雕刻”诠释 iOS 端 UI 界面的高效渲染》 。如何构建基于 Graver 进行异步渲染的动态化框架(MTFlexbox),成为首先需要解决的问题。

“拼控件”到“画控件”

通过对系统 UI 渲染流程分析不难发现:唯一确定一个视图展示仅需要确定视图布局信息、内容信息、渲染信息三个要素。含义如下:

  • 布局信息:UI 控件的大小、位置和展示层级。
  • 内容信息:UI 展示的文本、图片等。
  • 渲染信息:包括UI控件的背景色、展示字体字号、透明度、边框等控件视觉属性。

Graver 的每个绘制元素通过 WMMutableAttributedItem 来表达内容信息、渲染信息,CGRect 表达绘制元素的大小和位置。渲染整个过程除画板视图外,完全没有使用 UIKit 控件,最终产出的结果是一张位图(Bitmap)。如果能通过一棵树形结构组织所有的绘制元素即绘制结点树,即可按照递归遍历的方式“画控件”来转义“拼控件”构建视图。

接下来,我们需要思考如何建立 MTFlexbox 的数据结构与绘制结点树之间的关系,并且保证该转化过程完全异步化。

构建虚拟结点树

如开篇动态布局框架章节 MTFlexbox 的原理所描述:在相继完成模板树构建、数据绑定之后即进行了视图树构建。然而,出于功能划分考虑、兼顾保留 MTFlexbox 的系统 UI 渲染引擎能力以及构建绘制结点树需要的必要信息考虑,需要构建一个中间数据结构:虚拟结点树。它应包含树形结构的层级信息、Flex 属性信息、数据解析处理后的内容信息以及基本的渲染信息。虚拟结点树是既能构建 UI 控件树也能构建绘制结点树的“桥梁”。

设计数据流

通过上述思路分析,确定了关键数据结构:虚拟结点树、绘制结点树。接下来,我们需要思考如何构建虚拟结点树到绘制结点树的数据流。

在前端有两个重要的概念:回流重绘

  • 回流:当我们对结点树的修改引发了几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来,这个过程就是回流(也叫重排)。
  • 重绘:当我们对结点树的修改导致了样式的变化,却并未影响其几何属性(比如修改了颜色或背景色)时,不需重新计算元素的几何属性,可以直接为该元素绘制新的样式。

参考前端技术思想以及考虑单一职责原则,在虚拟结点树与绘制结点树中间构建 Fat 型渲染结点树和 Thin 型渲染结点树。Fat 型渲染结点树负责保存原始数据以便做逻辑处理,Thin 型渲染结点树负责保存位置、大小和内容信息。当有修改不影响几何尺寸变化的情况下,仅重新生成 Thin 型渲染结点树的内容信息即可。

线程安全考虑

提高渲染性能的关键,即是全力保证主线程的最小资源开销。因此,需要思考如何保证虚拟结点树到绘制结点树的转换过程是线程安全的。Facebook 开源的跨平台布局引擎 Yoga,提供了通过 UI 视图树中 Flex 属性计算得出每个 UI 控件的位置和大小。然而,提供给 iOS 平台的插头类是基于 UIView 的,即布局计算过程必须在主线程。需要基于 Yoga 核心逻辑重新封装基于渲染结点树的计算逻辑,以保证布局计算是线程安全的。如下图所示:

架构设计

有了上述的思路分析,接下来我们开始着手 Graver 接入 MTFlexbox 的架构设计。Graver 需要作为独立渲染引擎存在,并保留接入多种动态化框架的可能,这是出于架构设计的灵活性和扩展性的考虑。接入层命名为 M-Graver,其上层基于 MTFlexbox 进行扩展但可灵活插拔,下层基于 Graver 渲染引擎,如下图10所示:

图10 M-Graver 架构

M-Graver 是线程安全的,其主要分为解析层、聚合层、布局层和预备层。下面对各层分别做简单的介绍:

  • 解析层:关于输入定义了一个不依赖于上层 DSL 描述语言的标准化虚拟结点树结构;解析层主要进行虚拟结点树到渲染结点树的转换,涉及标签解析、渲染信息解析、统计信息解析以及 Flex 属性解析。
  • 聚合层:完成渲染结点树的最后构造,涉及可见性、交互性等处理;关于事件绑定也是在聚合层完成,每个事件都以行为参数化的形式绑定到相应渲染结点上。
  • 布局层:利用 Yoga 完成每个结点的 Frame 计算以及层级信息处理,同时将渲染信息等内容转换到 Graver 框架可识别的数据。
  • 预备层:进行坐标系转换,并拍平带有视图层级结构信息的渲染结点树,剔除无效渲染结点(如可见性、size 为0等),屏蔽掉由于视图层级原因导致被完全遮挡的渲染结点,最后根据渲染结点生成绘制结点构建绘制结点树,交由 Graver 提供的画板视图进行绘制。

技术难点

按照上述思路分析完成架构设计,但在实施部署的过程中也遇到了不少的技术难点和问题。如:动态布局框架 MTFlexbox 创建至今已两年有余,因业务的快速发展而产生了一些技术“负债”。为了保证不影响线上原有的业务逻辑,所以在进行 MTFlexbox 的模板树到虚拟结点树,再到 UI 视图树的技术升级改造过程时,尤其需要关注各种“蛛丝马迹”式的细微逻辑。

另外,在将异步渲染引擎 Graver 接入 MTFlexbox 的过程中也遇到了诸多问题,包括如何构建基于位图的事件处理系统,跨渲染引擎的技术融合,一些极端场景下的绘制效率瓶颈等等。下面将逐一展开阐述。

1. 如何基于位图进行事件处理

由于视图最终通过渲染位图来呈现,这就需要建立基于位图的事件处理系统。如前文所述,渲染结点树记录了每个控件的位置、大小信息以及层级结构,基于此可仿照系统事件处理逻辑进行基于位图的事件处理系统设计。

在视图展示期间,画板视图收到事件响应通知后(如点击了画板视图中标号为5的红色按钮),根据位图对应的渲染结点树存储的各控件布局、层级和渲染信息,逐层遍历找到需要响应的渲染结点,如果涉及信息修改则变更其在渲染结点树中的渲染信息,触发再次渲染的同时执行该渲染结点绑定的事件方法。遍历渲染结点树的输入是系统基于画板视图返回的点击位置,其遍历过程与系统 UI 事件查找过程比较相似。事件处理过程如下图11所示:

2. 系统 UI 控件与绘制元素的融合问题

从美团业务特征出发,图文组合占据多数 UI 场景。然而也存在诸如动效等无法依托 Graver 进行图文渲染的情况。因此,需要考虑跨渲染引擎的渲染融合问题。在 M-Graver 的预备层遍历渲染结点树时,可以根据当前结点是否为原生结点决议树拆分,如果是原生结点,将该结点连同其子树“直系”绘制结点从渲染结点树中拆分下来,以该结点为根结点的子渲染结点树,生成对应的绘制结点树,多个子渲染结点树的根结点,构成了以画板视图为单元的画板视图树。如下图12所示:

图12 结点融合

为了便于理解,我们给出以下几个名词的解释说明:

  • 绘制结点:渲染结点树中的结点如果可以转化成绘制结点树中的结点,则称之为绘制结点,最后通过 Graver 进行渲染。
  • 原生结点:渲染结点树中的结点如果不能转化成绘制结点树中的结点,只能转化成系统 UI 控件则称之为原生结点。
  • 结点变异:以一个二叉渲染结点树为例,左子结点是原生结点,右子结点是绘制结点的情况下,由于左子结点先于右子结点添加到父节点,可能存在层级颠倒问题。这时右子结点需要强制转为原生结点,维持正确的层级顺序,即结点变异。
  • 画板视图:继承自 UIView 的普通视图,其内部封装了一系列的基于 Graver 的渲染逻辑。

树拆分的过程还涉及到兄弟结点层级颠倒以及布局交叉等问题。兄弟结点层级颠倒问题通过结点变异来解决。布局交叉问题存在于判定渲染结点树的结点是绘制结点或原生结点之前,由于布局原因存在视图交叉。布局交叉问题通过新建画板视图插入来保证层级正确以及绘制正确。由于篇幅有限,这里不再赘述。

3. 极端场景下的绘制效率瓶颈问题

从产品交互层面看,为了提高屏效往往存在多向滑动的视图组件场景。如横滑 Scroll 组件,其特点是需要通过滑动才能逐渐看到所有的视图内容。通过异步渲染绘制位图来实现的情况下,存在单一并发渲染任务计算逻辑繁重的问题,从用户体验层面看容易造成“白屏”现象。为解决该问题,将视图卡片渲染过程分解,进行增量渲染,采用渐进式的方式减少空白页面等待时间。根据待展示区域在屏幕中相对位置进行区块划分,通过队列集中控制绘制操作。以此进一步提高并发效率,并减少渲染过程的非必要系统资源消耗。

区块划分

区块划分策略的实质是绘制结点树的拆分,将绘制结点树中不存在布局交叉的子结点树进行逐一拆分,每个拆分下来的绘制结点子树即为一个区块,同时要设置最小块策略,否则拆分粒度太小反而会因为过多的线程并发造成性能瓶颈。

分块绘制

以下图为例说明分块绘制逻辑。在滑动过程中,若本地缓存有此区域绘制结果,读取缓存并直接通知主线程展示,如例4中 X4’。否则,将该区域加入队列,以块为单元进行并发绘制,绘制完成后更新缓存,再通知主线程展示,如例1中 X1’,例2中 X2‘,例3中 X3’。对划到屏幕外的区域,从队列中清除,终止绘制操作;若此区域已绘制完成,则通知主线程清除此区域的展示,如例2中 X2,例3中 X3,例4中 X4。

业务应用

在完成“拼控件”到“画控件”的思路探索与技术落地之后,需要发挥其价值,将其部署到线上进行业务实践应用。动态布局框架 MTFlexbox 的跨平台代码复用能力对业务开发效率有了大幅提升。从产品层面看,在原有资源不变的情况下,达到了高效支撑业务迭代的效果。MTFlexbox 动态布局框架在经历了一次联合共建的“洗礼”后渲染性能得到大幅提高,变得愈加成熟、完善。在过去的半年多期间,我们采用异步渲染引擎 Graver 的 MTFlexbox 已先后应用在搜索结果页、美团首页等核心流量区业务。下面列举部分应用案例:

采用异步渲染引擎 Graver 的 MTFlexbox 绝大多数应用场景为列表级应用。如上图所示,所有视图卡片均为采用 M-Graver 实现的动态模板。截止到发稿,覆盖搜索结果页36个动态模板,覆盖首页42个动态模板,业务应用累计覆盖78个动态模板。

数据指标

以业务应用美团 App 新版首页为例,分类频道卡片以下全部为 MTFlexbox 实现的动态模板视图卡片。由于采用异步渲染引擎 Graver 的 MTFlexbox 具备了在线程安全条件下进行测量、布局、渲染,美团首页接入后滚动 FPS 提升明显,对于上拉加载过程的 FPS 提升更为明显。因此,列表使用体验变动更加顺畅。美团首页的50分位滚动 FPS 接近59,上拉加载 FPS 接近满帧。详细数据如下图所示:

总结复盘

从业务场景作为出发点和原始驱动力,如何改善动态布局框架的渲染性能问题,从本质上讲是解决业务迭代演变时带来的用户体验问题。这里有以下几点经验可供大家参考:

  • 在项目设计阶段要权衡考虑技术方案全景,作为技术方向规划,不做临时方案;架构设计要兼顾合理性、灵活性、扩展性。
  • 期间也会遇到诸如原生结点和绘制结点如何融合、事件处理系统怎样建设、如何分区绘制等一系列问题。保持开放心态,作为探索性项目在方案细节上有很多可行性,充分讨论、盯紧目标,不走极端。
  • 在跨部门协作项目中,尤其要关注项目管理、会议记录、里程碑等,同时保持高频的沟通。

最后,借用朱光潜先生在《艺文杂谈·谈对话体》中提到的一句话作为结尾:“疑难是思想的起点与核心。”

参考资料

  • 美团移动端动态化实践
  • 深入了解浏览器的页面渲染机制

作者简介

洋洋,美团点评资深工程师。

柏泉,美团点评高级技术专家。

晓宇,美团点评研发工程师。

Comments are closed.