列表容器&事件链如何帮业务提升发版迭代效率? | DX研发模式
熌电、中民2022-06-17

DX全称DinamicX,目前是在淘宝乃至整个阿里集团内广泛使用的Native动态化方案,核心优势是性能和稳定性。过去几年一直有其他淘宝/集团的外部文章中有涉及到DX,但DX一直没有对外做过完整介绍,对外界来说这两个字母颇有些神秘色彩。本系列文章《DX研发模式》我们就将拉下它神秘的面纱,看看过去两年 DX 在做什么。

本文主要阐述了DX在21-22年新增的两块新feature,RecyclerLayout列表容器以及事件链。全文首先介绍了两个功能诞生的背景,然后分别阐述两个部分方案设计以及最后的落地成果。

背景

容器背景

列表类的容器是业务方构建页面时最常用的交互形式,在RecyclerLayout组件上线之前,业务方如果想使用DX框架构建业务页面有两个选择,自建列表容器或者使用DXC容器(DXC容器的底层基于Native的 UICollectionView/RecyclerView控件,可以通过特定的数据协议渲染出页面。页面布局样式、坑位模板信息、业务数据都在数据协议中描述,坑位使用DX模板渲染),而这两种方案目前都面临动态化、开发效率、复用性等方面的问题。

容器样式动态化

业务方自建列表容器面临容器样式动态化的问题,通常情况下自建的列表容器都是通过Native代码使用UICollectionView组件构建的,所以大部分涉及到容器样式以及交互方式的变动都需要Native发版,而电商业务的特点是业务会根据大促节点和流行趋势频繁的变更以及上线下线,Native两周到一个月的发版频率无疑会制约业务的快速迭代。所以自建列表容器在动态性以及效率方面都会面临不小的问题。

开发效率和复用

使用自建列表容器的情况下,列表布局的各种样式、下拉刷新、分页、组件间的嵌套联动交互等功能都需要大量的开发工作量。并且不同业务页面样式以及交互方式的变化多端导致容器的复用性很低。

视图和数据耦合

使用DXC容器的情况下除了面临容器样式动态化的问题,还有页面结构高度依赖服务端数据,服务端同学需要感知页面结构,并且需要将业务模型数据根据DXC容器协议转换成页面渲染数据。当页面结构发生变更的时候客户端和服务端的沟通以及调试成本较高。

事件链背景

随着DX 本身的发展,以及像 RecyclerLayout, ViewPager 等页面级别组件的出现,我们在 UI 的动态性以及丰富上完成了更进一步的提升,基本能够满足业务方在在大多数场景下的 UI 动态调整的诉求。但是在事件逻辑层面一直未做较大的变更,这也使得事件逻辑层面的能力已经无法满足业务快速发展的诉求。

原有方案不足
原有的事件方案主要存在以下几点不足:

  1. 无动态性:之前 DX 提供的比如说处理一个点击,或者View 上屏事件,主要是通过 Native 的注入EventHandler ( EventHandler 是DX 在Native定义的一种事件回调接口,业务方可以通过实现该接口来接受类似于点击等的事件回调,下文统一称为EventHandler )来进行实现,因此一旦有这方面的修改就必须依赖 native 的发版。给业务带来了的放量,定投等成本,无法做到快速迭代;
  2. 复用性差:之前的 EventHandler 里面实现了一个完整的行为逻辑,里面包含了很多特定上下文参数拼装以及业务逻辑,很难在被其他的地方进行复用。这就使得业务方每次不得不定义新的 EventHandler, 这不仅带了不少包大小的问题,越来越多 EventHandler 也给维护带来很大的成本;
  3. 开发成本高:因为原有的 EventHandler 是通过 native 进行开发的,这就需要双端(Android/iOS)各自投入人力进行开发。不仅开发成本高,而且对于一致性,以及后续的维护迭代都有不小的挑战。

RecyclerLayout容器

RecyclerLayout容器设计的初衷就是为了解决业务遇到的上述几个痛点。图1是列表容器的技术大图,通过技术大图可以发现RecyclerLayout组件和LinearLayout和FrameLayout等组件一样是DX的基础布局组件,共享DX SDK的基础布局、动画、事件等能力。RecyclerLayout组件底层基于 Native列表容器进行实现的,同时我们也在对应的列表容器进行了一系列的优化措施,保障列表的滑动流畅性

图1

容器组件化解决容器样式动态化

RecyclerLayout继承自BaseLayout布局组件,所以RecyclerLayout本身就是一个DX基础组件,兼容DX渲染框架对于组件位置、尺寸、背景色、显示隐藏等属性的设置,如果想修改容器的样式只需要在XML中修改RecyclerLayout组件的属性并将模板发布上线即可。因此使用RecyclerLayout组件可以轻松的解决容器样式动态化的问题。

内建常用功能列表

内建了业务常用的列表布局样式、列表嵌套联动功能、操作列表数据的原子能力、获取列表信息的表达式、下拉刷新和分页组件。并且这些功能大部分都可以通过简单的属性配置方式启用,大大减少了构建业务的工作量。

支持常用列表布局样式

RecyclerLayout目前提供的列表布局样式包括通栏、瀑布流、吸顶、混合布局以及左滑菜单等布局及交互样式,基本涵盖了电商业务页面常见的样式。

图2

内建下拉刷新和加载更多组件

下拉刷新和加载更多功能是列表场景下比较常用的功能。为了降低业务使用的成本,RecyclerLayout容器内建支持下拉刷新和加载更多功能,提供默认样式+样式配置和完整组件替换两种自定义方式。

图3

默认样式+样式配置

默认样式和手淘通用下拉刷新及加载更多组件样式保持一致。下拉刷新组件支持刷新文案、文案字体大小、文案字体颜色配置。加载更多组件支持异常文案、nodata文案、文案字体大小、文案字体颜色配置。

完整组件替换

如果以上样式样式的自定义还不满足需求的话,可以将下拉刷新和加载更多组件完全替换掉。

内建常用的原子能力和表达式

原子能力

RecyclerLayout容器结合事件链使用可以实现逻辑的动态化,SDK也提供了下列列表容器常用的原子能力可供开发者开箱即用。主要包含增删改查以及下拉刷新和分页组件状态变更。

表达式

使用表达式可以实时的获取列表相关的一些信息,在使用事件链时可以使用下面的表达式,如果需求无法满足也可以自定义表达式。

支持列表嵌套和联动

业务在使用列表容器的过程中经常会有容器嵌套的诉求,例如订阅首页的多Tab场景就需要三层列表容器的嵌套以实现横滑Tab的交互。为了实现容器嵌套的能力,除了RecyclerLayout列表容器我们还提供了ViewPager容器以及TabHeaderLayout组件。

图4

使用高性能的Native列表容器

从图1的技术大图中可以看出RecyclerLayout组件底层基于Native列表容器。Native列表容器提供了各种列表布局样式的支持,以及异步计算高度、数据源diff、高效缓存复用实例等一系列性能优化方案保障列表在快速滑动的情况下的性能表现。

使用Remote Template拆分大模板

提供了配套的Template组件,可以将大模板中可以复用的模块拆成一个个小的独立子模板,然后使用Template组件引用这些子模板组装成大的模板。使用Template组件不仅可以提升模块的复用性,当大模板中某个子模板的样式需要更新时,只需要重新发布一遍子模板即可,可以做到按需更新。另外列表容器一般用在页面级的模板上,页面模板中包含的布局以及样式远多于坑位级的模板。如果所有的内容都打在一个模板中,会大大增加模板的大小。大模板不仅会影响模板下载成功率,频繁的更新也会浪费资源。因此我们给Template组件设计了Remote模式。

技术方案

使用Remote Template可以将一个大模板按照功能模块拆成一个个小的子模板,主模板中只需要在Template组件中注明想要引用的子模板的name、version、url信息,DX SDK在运行时会自动的下载或者使用本地缓存的子模板数据挂载到主模板中。Remote Template组件目前不仅可以用在RecyclerLayout容器中,普通组件中也可以使用。下图是Remote Template的流程图。

图5

视图和数据解耦

使用RecyclerLayout的情况下,页面结构直接在XML模板中描述,服务端同学无需关注页面结构。开发职责更加合理,会显著的降低团队间沟通成本,提升开发效率。

以订阅首页的多Tab结构为例,如果使用DXC容器,服务端需要构造下面格式的协议数据。

图6

图6中的hierarchy字段对应的数据是DXC协议中描述页面结构的部分,组件的组合关系以及顺序会在hierarchy中描述,data字段的数据是每个组件的类型以及组件对应的业务数据,container字段的数据是每个组件对应的dx模板信息。可以看到视图信息和业务信息是高度耦合在一起的,这种数据结构适合在搭建场景下通过系统自动生成,而有些复杂业务是通过服务端拼成这种结构,每次页面结构的变动都需要客户端和服务端开发的反复沟通。

而使用RecyclerLayout列表容器构建页面的情况下,页面结构全部在XML模板中描述,和数据隔离。服务端只需要关注业务模型即可。页面结构的变动只需要客户端同学修改模板之后将模板发布到线上既可。下图7是订阅首页使用RecyclerLayout组件改造后的页面结构。

图7

事件链
下文主要介绍了事件链相关的几个核心的设计和在开发跟运行阶段碰到的核心问题以及其对应的解决方案。

方案设计

整体设计

事件链整体设计分为两大模块,端侧跟平台侧。其中端侧我们将其拆分完引擎层以及能力层,方便能力层能够供多团队复用。

平台/IDE侧

  1. 事件链相关的能力注册,权限管控
  2. 事件链的相关的编辑,智能补全
  3. 事件链的编译,目前我们将事件链直接编入了 DX 原有的二进制中,这使得端侧能够共享了 DX 模版管理中的所有功能,例如:模版的下载,模版的降级,模版的加载等等。同时这样也更好了保证了逻辑跟 UI 的同步
  4. 事件链相关的调试回放

端侧

  1. 引擎层:主要负责事件链的二进制解析,以及整体的链的调度工作
  2. 能力层:链中节点的能力实现

协议设计

事件链协议我们采用的是json格式,最开始主要是考虑为了跨端跟后续的扩展性。

这里面有几个关键点,首先这个json里面可以定义多条事件链,每条事件链之间也可以进行相互调用。

每条事件链是以main 为入口,通过next跟callback来进行串联。同时为了更好的排查事件链运行中的问题,我们在事件链中加入了全链路埋点的功能。

执行引擎设计

调度方式设计

顺序执行

通过next来进行连接两个原子能力的连接。例如点击按钮弹出toast并发送埋点,事件是按照顺序依次执行完成。

异步执行

该种方式要通过callback来进行承接,例如点击按钮发送mtop请求,并在请求成功后弹出成功toast。这种异步callback,不会阻塞原有的执行链。

核心挑战

挑战1:事件链的编辑调试

因为事件链的协议是一套基于json的协议,并且事件链支持原子能力之间相互嵌套,以及相互引用,最终导致代码量以及层级都可能会很多。这对于开发时的编写以及后期的维护都有着不小的挑战,我们目前采取的方案是通过IDE的相关配套来解决这两个问题。

编辑--可视化映射及代码块补全

此处主要是通过可视化的方式将事件链描述的原子能力的调用关系描绘出来,其不光支持简单的next节点关系,还可以支持经过一些逻辑运算的节点关系,例如经过triple(DX 内置的一个三目运算表达式)运算的,同时还支持可视区域与代码块的双向关联。

另外在进行事件链的编写时,可以针对于某个具体的原子能力,进行代码块级别的完整补全。

通过多个类似上面的这样的配套功能,极大的提高了业务方的事件链开发效率。

调试--事件链回放

虽然我们提供了比较好的开发环境,但在所难免的在实际运行的过程会出现结果不对或者运行异常的场景。为了解决这了问题,我们提供了事件链执行的回放功能。通过该功能可以完整回放出当前链执行的链路流程,以及在具体哪个节点发生异常,并且可以查看每个节点完整的属性。通过该功能可以帮助业务方很快的定位到当前链执行过程中的问题。

挑战2--环境与存储设计

为了保证事件链间的相互隔离,以及链中间各原子能力的相互通信交互,我们为事件设计了多个维度的存储机制以及单链间的隔离的上下文运行环境。

挑战3--线上监控/问题排查

因为事件链的整体执行链路比较长,且包含了同步异步等多种方式,因此一旦在线上发生异常后比较难定位具体是哪里出了问题。

为了解决该问题,我们给每条链增加了异常保护跟监控,可以线上实时的接收到魔兔(一款一站式的数据解决方案平台,可以接收线上的Crash,ANR等异常告警)的报警提醒。同时我们将整个运行过程接入了全链路埋点。通过全链路埋点,我们可以清楚的看到事件链执行的每个阶段,以及对应的原子能力参数和返回值,帮助在在线上也能够快速的定位问题所在。

业务落地

目前DX列表容器在手淘、天猫、千牛等客户端上有将近10个业务已经上线,其中部分业务的大量逻辑都使用事件链完成。

图10

业务收益反馈:

加速了业务的发版效率,帮助业务需求迭代不跟版率提升了30%以上。

支撑了更加复杂的多变的嵌套容器场景,以及多种配套的性能优化措施,整体的帧率也有着不错的提升

帮助业务精简了页面传输协议,服务的响应速度也有着15%左右的提升