Cube 技术解读 | Cube 卡片技术栈详解
此前,我们上线了《Cube 技术解读》系列首篇文章《支付宝新一代动态化技术架构与选型综述》,本文为 Cube 系列第二篇文章,针对 Cube 卡片技术栈做了深入解读,欢迎大家关注。
动态卡片的背景
从 Windows 时代开始,应用程序图标就成了用户(流量)的主入口,并且一直持续到移动端时代。图标即入口的方式,缺点是不直观,最少需要一次点击后才能接触到想要的信息。在此背景下,iOS 系统和部分 Android 系统实现了把内容和服务前置的卡片,举个例子,如下图 1 所示,苹果左一屏的卡片承载天气 &股市内容的展示。此外,鸿蒙系统也提出了类似的卡片场景,作为某种流量入口。实际上,在应用内部的卡片作为内容展示以及服务入口的场景则更为普遍,图 2 和图 3 分别是支付宝首页和招行银行的理财页面,其中每个小矩形都是一个卡片。对于运营来说,卡片样式和内容可以随时配置,不用等待应用版本升级,是某种刚需。
Cube 卡片概要
Cube 卡片是蚂蚁金服内部自研的一套跨平台动态化卡片解决方案,是服务于应用页面内的区域动态化技术,面向内容运营,帮助产品技术提高开发效率和运营效率。每一个 Cube 卡片独立嵌在原生页面内的一个区域,区域内容通过卡片模版进行展示。卡片的定位大致如下:
跨平台一致性
一套代码
效果对齐
动态化
界面结构 &样式动态化
业务逻辑动态化
高性能
极致的性能
极致的内存
这里展开讲下高性能。Cube 卡片追求的是接近 native 原生体验。我们定义了两个维度:
一个是极致的性能。在 Cube 小程序能力的基础上,我们去掉了一些复杂的 css 能力,例如伪类伪元素、inline/block 等,同时也对 js 的能力做了限制(Cube 卡片使用 quickjs 作为脚本引擎)。此外,我们还对 quickjs 做了一些优化,包括不限于离线 atom 编译优化,异步 gc 优化等。我们也引入了 wamr 作为 quickjs 的“协处理器”,支持用户使用 javascript 和 assemblyscript 混合开发。这样用户可以用 assemblyscript 一些热点函数或者模块。
另一个维度是极致的内存。在信息瀑布场景无限下拉,Cube 卡片的内存增长接近 Native 卡片。我们对卡片的能力做了比较精细分级,通过在开发时配置,减小运行时的内存消耗。下图展示了一个简单卡片,如图所示 Cube 卡片的工程目录,以及钱包某个卡片的真实代码和运行效果。
Cube 卡片的生产 &工作流程
研发期
本地开发
Cube 卡片配套独立的开发工具,支持卡片的编译、日志输出、实时预览等功能,vue 作为当前开发模版的 dsl 语言,支持 js、css 编辑卡片样式。
卡片管理
卡片本地开发完成后,通过卡片管理后台将卡片编译产物上传发布,可以对卡片进行版本管理,卡片发布后就可以在客户端进行卡片的动态更新。
运行期
为了方便端上业务接入 Cube 卡片,我们引入了一个 Cube 卡片容器(CardSDK)的概念。CardSDK 把一些和业务层/服务端联系紧密的,且通用能力做了一些封装。例如我们通过 CubeCardSdk 从服务端拉去卡片和业务数据。此外 CardSDK 也负责常用的 JSAPI、第三方组件的接入。这样 Cube 卡片能够更专注于卡片产品本身。
核心系统架构
Cube 卡片的系统架构主要包括 JSEngine、CardEngine、RenderEngine 和 Platform 几部分,绝大部分代码都是 C++实现。
JSEngine
主要负责卡片 js 逻辑执行和卡片数据变化监听,从而支持开发者在卡片内部写一些业务逻辑能力实现卡片内容和样式的动态变化。
因为卡片场景对性能要求较高,综合包大小和性能等方面考虑,我们选择了 quickjs 作为我们的 js 基础引擎库,同时实现了一个非常小的 js 响应式框架(JSFM),用来支持卡片内的逻辑代码能力。
CardEngine
主要负责卡片数据的解析和绑定、卡片逻辑渲染、构建 DOM 指令、JSAPI 管理、JSBinding、Native 事件通信等。
卡片 DOM 树的初始化构建过程,我们并没有把它放在 js 运行时,而是在卡片实例初始化链路中直接通过 C++进行指令生成和树构建,一方面是为了保持 js 框架更小更快,另一方面 C++的运行效率更高。
RenderEngine
后端渲染底座,负责卡片布局计算、样式解析、Layer 计算、自绘制组件、同层渲染、光栅化上屏等过程,以及手势、动效等交互效果。
Platform
平台相关接口,包括原子 view 封装、Canvas API、三方组件扩展协议、动画 api 等。
线程模型和数据模型
线程模型
Cube 卡片生命周期内的主要线程包括业务线程和引擎线程,业务线程是卡片数据的初始化阶段由业务发起执行,是卡片生命周期的 beforeCreate 阶段。引擎线程是所有卡片生命周期运行阶段的共有线程,主要包括 Bridge 线程、Render 线程、Paint 线程和 UI 主线程。
Bridge 线程
js 运行时线程,也是 Dom 节点数据查询和处理线程,因为基于 Cube 卡片小、快的定位,js 逻辑只是卡片一个辅助能力,不具备过于复杂业务逻辑能力,所以 Bridge 线程相对较轻,并设计为单线程模式。
Render 线程
渲染相关数据计算线程,包括渲染树构建、节点层级计算、Layer 分层绘制计算、手势数据计算以及渲染任务构建,Render 过程主要涉及树的递归计算过程,相对渲染过程耗时很短, 设计为单线程模式。
Paint 线程
绘制线程,执行卡片节点分层绘制及光栅化任务。Paint 线程并不是一个固定的线程,根据当前任务模型,Paint 线程可能是主线程,也可能是一个线程池里的子线程;在同步渲染模式下,Paint 线程直接是主线程;而在异步渲染模式下,通过一个线程池来实现 Paint 任务的并发渲染,提高渲染效率,例如在列表滑动场景。
UI 主线程
UI 操作主线程,即为目前的平台线程,主要包括手势识别、UI 上屏和三方扩展组件的数据更新等。
除了以上涉及的主要线程外,还有埋点和监控相关的 playground 后台线程,整体优先级比较低。整体的线程模型设计,最大限度减少 UI 主线程压力,提高卡片并发渲染效率。但目前还有一些不足,包括 UI 线程切换频繁、Bridge 线程越来越重等,后面会继续优化线程模型。
数据模型
和线程模型对应的数据模型主要包括三棵树:NodeTree、RenderTree、LayerTree,初此之外,还存在一个临时的 PaintTree;
NodeTree
卡片原始节点树,对应前端的 Dom 树,引擎会根据 NodeTree 做样式解析和布局计算;
RenderTree
渲染数据树,这是一颗变形树,很多情况下它的树层级结构和 NodeTree 是一样的,其实当初在设计定义引擎数据模型的时候,我们讨论过到底要不要这棵树,有没有必要存在这样一颗和 NodeTree 层级一样的树,最终我们还是保留了,原因是这棵树可以比较灵活的调整树关系,如果把卡片分为布局阶段和渲染阶段,那么这颗树就是渲染阶段的源树。
事实证明我们的决定是正确的, 我们后续支持的 zindex/static 等能力,都是因为这棵树的存在可以在引擎层很好的去支持, 而不用在平台层去模拟实现这种层级变更能力从而导致很有限的场景支持,包括以后我们做渲染快照技术也可以从这颗树去考虑。
LayerTree
LayerTree 树,顾名思义就是一个分层树,在 RenderTree 基础上对节点进行分层,同一层的节点在同一个渲染任务管线内做绘制光栅化,不同层之间相互独立,可以并发渲染。
PaintTree
PaintTree 是一个临时树,其实严格的说是一个拷贝树,是通过 RenderTree 拷贝一个子树,每次发生渲染时临时生成,当然也会做些节点优化处理,例如被完全盖住的节点会被优化调,避免重复渲染。每一个 layer 上存在一个 PaintTree,通过 PaintTree 进行节点绘制生成光栅化指令或位图。
高性能列表渲染
对于列表内使用卡片的场景,主要考虑的是卡顿影响,尤其是中低端机设备。Cube 卡片支持异步渲染,所以在列表场景下可以很流畅,同时因为支持多线程并发能力,可以多张卡片并发渲染,所以在异步渲染条件下也不会有明显的白屏效果。
Native 技术优化
我们期望卡片服务于页面内区域化内容动态展现和简单业务逻辑,更多的是面向移动端开发者。即使我们使用的卡片 DSL 语言描述是前端语言,我们也希望能够对 CSS 的使用做约束、支持有限的 CSS 能力,但同时也希望尽可能覆盖到一些开发者常用的 CSS 能力。
所以我们针对 CSS 能力做了一个专项工作,和前端团队技术同学一起做了 Cube 卡片 CSS 能力规范,对 CSS 能力做了约束限制。即便如此,在 Native 渲染引擎下,想非常好的去支持这些能力,也是有很多困难,包括 zindex 的支持、overflow 等,因此我们也基于一些依赖的平台能力也做了优化处理。
Layer 容器
我们引入 Layer 容器概念,前面介绍数据模型时,提到了 LayerTree,每一个 Layer 节点是一个独立的渲染容器,由平台 View 作为 Layer 容器来渲染其他虚拟节点。如果按照常规的做法是一个 View 对应一个渲染容器,我们使用两个 View 组合为 Layer 容器(iOS 使用 CALayer),将内容层和逻辑层分离,这样做的好处很多,例如 layer 节点的 shadow 绘制限制裁剪问题、内容层的画布切割优化等。
手势改造
手势的优化改造主要为了解决平台系统手势分发能力的限制,不管是 Android 平台还是 iOS 平台,系统对手势的分发处理都有一些限制,例如兄弟节点不能分发事件(iOS)、超过父节点区域无法接收事件(影响 overflow 能力)等,所以需要对手势进行改造。
因为卡片渲染支持三方组件扩展,为了不影响扩展组件的事件响应,我们基于 Layer 容器接管改造系统手势行为,内部进行容器节点的手势分发管理,而对于存在三方组件混合渲染的场景,Layer 容器和三方组件之间的手势分发保持系统行为。
光栅化
Cube 卡片渲染过程包括指令渲染和位图渲染两种渲染模式,这两种模式会在不同场景条件下切换,用来优化不同场景下性能,例如帧率和内存。位图渲染在 Android 上相对比较复杂。默认是用 Bitmap 作为离线渲染的缓存,缺点是引入一次额外 cpu/gpu 内存拷贝并且无法充分利用 GPU 资源,优势是兼容性好。我们尝试过使用 textureview 作为离线渲染缓冲,发现 6.0 以下设备存在严重的兼容性问题,而且不同设备之间的稳定性差别巨大。
同时光栅化能力依赖平台系统的 Canvas API,有些高阶方法会涉及硬件加速的限制,包括 shadow api 以及系统对 glRender buffer 的限制(Android 平台),我们也对大画布场景做了视图切割分段渲染来保证渲染性能。我们同事也在着手用 Skia Canvas api 替代平台层的 Canvas API。
同层渲染
Cube 卡片把三方组件当作独立一层 layer 单独进行数据更新,可以非常方便高效的接入扩展的三方组件。基于系统的 UI 能力,使扩展组件在卡片内天生统一渲染。同时支持组件在不同卡片上的复用。在实际的业务场景中,同层渲染也带来了很多额外的问题。诸如地图/视频/动画等组件,一般会伴随着较大的性能内存开销。这些开销对卡片的渲染会有负面影响,尤其在列表滚动时。对于地图/视频组件,我们配合组件提供方 case by case 的解决问题,并且试图在卡片上线时设置卡点。对于动画组件,Cube 持续的在扩展属性动画/帧动画能力,并且内置 canvas 能力。
Cube 卡片的业务现状和未来规划
目前 Cube 卡片已经服务钱包的首页、证券(股票)、卡包、出行等 20+的业务场景,日 pv 超过 100 亿。在未来相当长的一段时间内,我们的主要精力还是会集中在钱包内部的业务场景,把存量的 native 卡片/h5 卡片 cube 化。服务好钱包内的场景,一方面需要把开发者体验做好,诸如开发调试工具链条,另一方面要持续的优化基础性能,诸如追求更小的包体积,更低的内存等。
卡片未来规划一个重点方向是商业化,即把 Cube 卡片输出到中小型互联网公司以及金融企业。这部分的工作已经启动了一段时间,预计年底前会作为 mpaas https://tech.antfin.com/products/MPAAS 的一个扩展功能发布。
卡片未来规划的另一个方向是物联网设备(例如 RTOS)的应用开发栈。准确说不是 Cube 卡片,而是 Cube 卡片和小程序的某种中间形态。物联网设备的界面一般比较简单,近似卡片;但是又需要多个“卡片”之间的路由能力,更接近于应用的形态。这样一个混合形态既能保留 Cube 卡片在内存/性能/包体积上的优势,又能满足物联网设备应用开发的诉求。根据我们的调研,大部分 RTOS 应用开发环境还是停留在传统的 c 语言,效能和动态性都不不理想。对于开发者来说,Cube 也许是一个选择。
预告
如你对该系列文章感兴趣,感谢大家持续关注本公众号【阿里巴巴移动技术】,下一篇 Cube 技术解读文章我们再继续畅聊。
关注我们,每周 3 篇移动干货 &实践给你思考!
版权声明: 本文为 InfoQ 作者【阿里巴巴移动技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/56336516d0c90382434c77d98】。文章转载请联系作者。
评论