🙋🏻♀️ 编者按:本文为《Cube 技术解读》系列第六篇文章,作者是蚂蚁集团客户端工程师晟仑,对于 Cube 渲染引擎来说,在 OTT 上,面对的是一个全新的场景,既需要解决 OTT 设备的性能问题,也需要更合理的焦点引擎能力的支持,本篇带来大屏小程序探索实践,欢迎查阅~
背景
支付宝客户端有极强的动态化诉求,不论 iOS 还是 Android 平台,重新分发软件包从时间上,效率上难以满足产品运营的要求,因此客户端动态化技术应运而生。
Cube 起源于 Native 页面的动态化诉求,随着小程序的出现,Cube 融入了支付宝小程序技术栈,产品形态为轻量级的支付宝小程序解决方案(相对于使用浏览器作为核心的 Web 小程序)。作为一个轻量级引擎,Cube 小程序具有体积小、启动快、内存占用低的特点。而在 IOT 领域,因 Cube 以上的优势,于是衍生出了适合大屏的小程序技术栈。
何为大屏小程序?
所谓大屏小程序,是以 Cube 小程序技术栈 为载体,运行在智能电视或智能机顶盒等设备上的一种小程序形态。这些设备的主要特点是:
- 以 Android 系统为主,系统版本普遍较低,有些设备依然停留在 Android 4.2,Android 4.4 以下设备占比在40%+;
- 内存低,512MB 以下的设备占比超过 20%,1GB 以下的设备占比超过 60%;
- CPU 主频低,双核占比在 20% 左右,有的只有 1.5GHz;
- 和手机设备相比较,按手机淘宝的设备评分体系为参照的话,全部都是低端设备。
正是因为这些智能电视或智能机顶盒天然存在的硬件上的瓶颈,且运行在这些智能电视或智能机顶盒上的Android 应用程序在版本发布上受行业广电监管以及硬件厂商的限制诸多,每年只有几次的版本发布迭代窗口。所以亟需一种更小、更快、更省的动态化技术栈来解决用户设备升级周期长,客户端版本长尾问题。而 Cube 小程序技术栈,从设计之初就具备更小、更快、更省的特点,因此成为智能电视或智能机顶盒(以下简称为 OTT 设备的不二选择。
对于 Cube 渲染引擎来说,在支付宝钱包中,面对的是手机端的场景,而在 OTT 上,面对的是一个全新的场景,既需要解决 OTT 设备的性能问题,也需要更合理的焦点引擎能力的支持。
焦点引擎
何为焦点引擎?上面提到了何为大屏小程序以及大屏小程序运行的 OTT 设备,而这些 OTT 设备的交互方式,就是通过遥控器来操作,这一点和触控设备完全不同。遥控器通过 Android 系统的按键系统,进行事件分发,来实现应用程序的交互,那么这里就不得不提到我们的焦点引擎,也就是 FocusEngine 了。
所谓 FocusEngine,实际包括几大部分:FocusNode (焦点 View )、FocusTree (焦点树)、FocusFinder (焦点查找器)、FocusState (焦点状态)、FocusEffect(焦点效果)。
而原有的 Cube 小程序技术栈不具备FocusEngine 的能力,也就是说渲染引擎中缺少最后一棵树 FocusTree,更没有焦点查找器、焦点状态以及焦点效果的支持。关于 Cube 小程序渲染引擎中 LayoutTree,RenderTree,LayerTree,此处不再进行额外赘述,详细了解可参见《Cube 渲染设计的前世今生》该篇中常见术语和数据模型的介绍。
实现细节
上面我们提到 FocusEngine 包含的几个主要部分,下面我们来看看 Cube 小程序渲染引擎中的无名英雄------ Focus 的实现。
FocusNode (焦点 View )
FocusNode 是用于获取遥控器按键事件的对象,也就是说当小程序的 AXML 中一个 view 节点具备了focusable 属性时,那么在 Cube 渲染引擎中会映射成一个 FocusNode 节点,且会进行实体化。该节点具备主动获取焦点 requestFocus 和主动清除焦点 clearFocus 的能力,同时也可以监听 focus 的变化,可接收到 onFocusChange 的回调 onFocus 和 onBlur 事件通知
<view id="focus-control" class="button" focusable="true" onFocus="handleFocus" onBlur="handleBlur"> doRequestFocus() { my.createSelectorQuery().select('#focus-control').node().exec(function(ret) { const ref = ret && ret[0]; ref.requestFocus(); }) }
FocusTree (焦点树)
FocusTree 是与 Cube 渲染引擎中的 LayerTree 独立开的,一棵单独的实体节点的 Tree,它主要用于维护 LayerTree 中可聚焦 FocusNode 之间的父子关系。FocusTree 暂时无法通过单独的工具查看这棵树的结构,但挂在树上的 FocusNode 可通过 DevTools 的 Elements 或 Android 的 LayoutInspector 查看,后续会考虑支持进行独立树的 dump 后导出方便查看。焦点树结构示例如下:
FocusFinder (焦点查找器)
FocusFinder 是负责焦点搜索逻辑的核心查找器,内部维护着用于从当前具有焦点的 FocusNode 中查找给定方向上的下一个可聚焦 FocusNode 的算法(PS:这里的方向指的是遥控器操作的上下左右)。
在 Cube 的渲染引擎中,无论是 Layout、Render pipline,还是 Tree,代码均采用 C++ 来实现,而 FocusFinder 的算法实现亦是如此。我们在 FocusFinder 的实现上,以 Android 的 FocusFinder 实现为蓝本,实现了适合 Cube 渲染引擎的焦点查找算法,去除了 Android 本身的 descendant focusability 支持,采用默认FOCUS_AFTER_DESCENDANTS 的查找策略,且基于内部 FocusNode 的管理机制,不再额外提供 UserSpecifiedFocusNode 的支持,并支持了五种优先级查找策略,分别为 CENTER_FIRST(中间对齐优先)、LEFT_FIRST(左对齐优先)、RIGHT_FIRST(右对齐优先)、TOP_FIRST(顶对齐优先)、BOTTOM_FIRST(底对齐优先)。焦点查找策略举例说明如下:
如上所示,当前获取焦点的 FocusNode 为 A,而 FocusNode B 和 FocusNode C 在水平方向上与 Current Focus Node A 的 Rect 存在交集,因此在内部我们会通过带权重比的距离判断;FocusNode E,在垂直方向上与 Current Focus Node A 的 Rect 有交集,而水平方向上没有,所以更适合作为 FOCUS_UP 的焦点查找,而不是 FOCUS_LEFT 的查找;而 FocusNode B 和 FocusNode D,按照 Cube 的渲染引擎的规则,在水平方向上是允许跨列移动焦点的而垂直方向是不允许跨列移动焦点的 且 FocusNode E 会作为 FOCUS_UP 的优先选择, 因此,此时进行 FOCUS_LEFT 的时候,会优先选择 FocusNode B。
FocusState (焦点状态)
FocusState 是负责维护 FocusNode 节点焦点状态的管理者,内部维护 FocusNode 节点的焦点状态,因为在 AXML 下,当一个 View 节点获得 focusable 属性时,那么此时该节点会被标记成 FocusNode 节点。当 Android 系统按键分发时,FocusFinder 接收到查找的命令,会从 FocusTree 中进行 FocusNode 的查找,当找到最合适的 FocusNode 节点时,那么此时该 FocusNode 需要标记自己的 FocusState 为 focused 状态,与此同时,假设内部的 child node 没有 FocusNode,但有叶子节点 text 或 image,那么此时会标记 text 或 image 节点为 selected 状态。
FocusEffect (焦点效果)
FocusEffect 主要用来表示不同的节点对应的 FocusState 的焦点效果,在前端开发者视角下,主要通过 CSS 的伪类 :focus 来实现,如下代码片段:
.button { margin-top: 30rpx; margin-left: 15rpx; width: 450rpx; height: 200rpx; border: 2rpx red solid; } .button:focus { border: 2rpx blue solid; }
实际效果
如下图所示,高亮的 Item 所在的节点为当前获得焦点的 FocusNode:
体验优化
下面看下在 OTT 设备上 Cube 小程序的基础性能数据:(以淘宝特价版小程序为例)
因优化环节涉及到整体小程序技术栈,所以这里简单提一下小程序基础设施中的几大部分,其中包括:容器,前端框架,Cube 渲染引擎,脚本引擎。主要性能优化手段包括但不限于以下内容:
- 包大小:移除 OTT 设备不需要的能力模块,移除 AntUI、安全数据库、permission 管控模块,共享apk 宿主内 so。
- 脚本引擎:在支付宝钱包移动端上使用 JSC/V8 作为 JS 代码的执行引擎,IOT 设备上使用的是 QuickJS。
- 内存占用:
- 静态区块优化;
- JNI Global Reference 优化;
- Activity Context 上下文,虚拟机和 Native 的解耦,防止 Context 被 Native 持有,造成内存泄露;
- Native 中渲染树相关内存优化,内存泄露检测处理。
- 启动性能:
- 虚拟机优化方面,使用 Class Verify 抑制 + GC 抑制;
- 优化 Android InvocationHandler 动态代理在中低端设备耗时问题;
- 小程序产物 Bytecode 能力支持,减少小程序产物在脚本引擎下 Parse 和 AST 的阶段耗时,直接执行Bytecode。
开发体验
上面我们介绍完了和 FocusEngine 相关的实现细节,相信大家对于 FocusEngine 所包含的几个主要部分有了初步的认识和了解。通过上述关于 FocusNode 节点以及 FocusEffect 伪类的代码片段,不难看出,只要使用了 Cube 小程序技术栈,那么开发一个大屏小程序,和开发一个普通的支付宝小程序差异性是微乎其微的。
除了开发方式和普通支付宝小程序完全无二之外,在调试工具上也是可以使用 Chrome DevTools 来进行编辑和修改相应的可 Focus 的 DOM 节点的,如下图所示:
探索与应用
从拥抱 Native 开发,到拥抱动态化小程序技术栈开发,即解决了碎片化设备下的长尾瓶颈问题,又是产品技术的架构升级。在 CIBN 酷喵影视下,我们和业务方一道进行了大胆的创新和探索,最终实现了多种 Cube 小程序的产品形态。其中包括用于双十一战役的直播间半屏小程序、用于桌面首页大作业的 Tab 小程序、用于搜索广告下的嵌入式小程序、以及用于业务运营的全屏小程序,效果分别如下:
未来思考和展望
关于渲染引擎本身的思考,未来会进一步打磨整体的渲染链路,在性能上提升 Cube 小程序的启动性能。众所周知,解释性语言每次运行时都需要通过解释器对程序进行动态解释和执行。而这个过程少不了从 Parse 到AST,再从 AST 到 Bytecode 的过程,未来会进一步减少 Parse 和 AST 的过程,能充分发挥脚本引擎对Bytecode 的支持,提升启动性能。另外,也会逐渐减少对 Platform 层的依赖,使 Platform 层更轻量化,充分降低 Cube 渲染系统和平台本身的渲染系统的耦合,更好的完善提升渲染本身的能力。
在业务场景探索上,未来会与更多的二三方进行合作,无论是大屏端的 App,还是桌面 launcher,帮助他们解决版本长尾效应和业务动态性问题,当然也会有更多的场景的支持,譬如小游戏,3D 等。
如果你需要一种更小、更快、更省的动态化渲染技术栈,且具有更好的开发体验和行业共识,那么 Cube 也许是一个不错的选择。