Hippy 剖析

随着大前端技术栈的演变,各种跨平台框架层出不穷,本文选用 Hippy 作为跨平台框架,以 Hippy/Vue 为切入点,深入分析从前端到终端整个链路上的渲染流程。

一、为什么选择 Hippy

为什么选择Hippy

  • Hippy 性能高:性能优化可到秒开,对比 H5 优势非常明显
  • HIppy 集成小:Android 增量共 3.8M(so占2.9M),iOS 增量1M,对比 Flutter Android 集成增量 5.16M,iOS增量14.2M
  • Hippy 支持大:专门中台维护保证新功能迭代/问题响应

二、Hippy 整体架构

Hippy架构

Hippy 从技术栈的角度上划分为三层:

  • Java/OC:应用框架层,主要框架的 module、views、adapter 提供和绑定,以及使用的流程等
  • C++:内核层,主要的是对 V8/JSC 引擎的封装和运行,以及提供了 JSI 绑定的 基础模块,和 JS Bridge 和 JNI Bridge 两层模块的交互等
  • JS:驱动层,主要是支持 react/vue 在 hippy 上运行和渲染,并封装了对齐 H5 的基础组件和样式等

三、Hippy 框架剖析

1、Hippy/Vue 如何生成 DOM Tree

1)节点映射

我们书写一个页面的代码,并打印它最终传输到终端的数据如下:

hippy2.x框架

终端就是根据右边的 node 数据进行渲染的,它会组成一个 DOM Tree:

hippy tree

其中有每个节点的终端组件名称和属性样式,以及节点之间的关系包括自身节点和父子节点。那这个 hippy/vue 是如何生成一颗传给终端的 DOM Tree 的?

2)hippy/vue 运行原理

hippy-vue架构

  • 编译阶段:提前确定、不变的部分提前转换为终端识别的组件或属性
  • 运行阶段:监听 node-ops,构建 DOM Tree
  • 绘制阶段:insert/remove/update 放入 batchNodes, $nextTick 批量传输给终端

3)hippy/vue 模版生成

hippy-vue详剖

  • 基于 Vue 框架提供的 node-ops 钩子函数,拦截 DOM 的增、删、改操作,可应用在 Web、Server 或 Hippy 渲染中,这里转到 Hippy 框架中进行组装
  • Hippy/Vue 构造了一个 ViewNode 树,并保存在一个数组结构中,放入数组中的 node 已经是转换为 NativeNode 的终端结构,包括样式匹配、终端组件名称映射
  • 并在每个 tick 会检查是否有需要传输的节点,若有则批量传输给终端

4)hippy/vue 样式匹配

hippy-css

  • 编译时:CSS 样式文本会编译成 AST Tree,并放入 global[GLOBAL_STYLE_NAME] 全局变量中
  • 运行时:
    • 将 CSSRules 构建成 CSS 选择器,可被 id、tag、class 查询
    • 在 ViewNode 转成 NativeNode 时,会根据 id、tag、class 查询和获取节点的样式信息,并组装到 NativeNode 的 props.style 属性中

2、如何传输 DOM Tree

hippy-js传输

  • Hippy/Vue、Hippy/React 节点构建统一调用 UIManager 批量传输
  • js-native 提供 Hippy.bridge.callNative 等通用能力通过 JSBinding 到 C++,再调用到 Java/OC 层

3、如何根据 DOM Tree 渲染

终端接收到 DOM Tree 数据后,整体会经过两个阶段,第一个阶段是序列化还原到 DOM Tree 结构,第二个阶段是把 DOM Node 批量转换到终端真实的 Render View 组件并渲染。

为什么选择Hippy

① dom tree 构建:DOMManager 接收 UIManager 发送过来的节点数据组装还原 DOM Tree

② node 布局计算:调用 flex 布局引擎计算每个 node 的节点位置

③ render tree 构建:将 DOM Node 转换到终端的渲染节点 RenderNode,并传入对应的坐标位置和属性

④ render view 构建:批量将 RenderNode 节点转换到终端真实的组件 Render View 节点进行创建

⑤ render view 更新:更新 Render View,进行 children 的添加、调用 props 方法和 layout 排版

4、模块调用优化(JSBinding & JSI)

JS 到业务应用层一般要经过 JS -> C++ -> Java/OC,Android 比 iOS 多一层 JNI 传输,对于传输的数据比较大时,序列化、反序列化的耗时会成为性能瓶颈。于是有 JSBinding 和 JSI 的优化。

1)JSBinding

hippy-jsbinding

对于需要传给 Java 层的一般通过统一的 bridge 调用,有序列化、反序列化和 JNI 的耗时,于是就有了直接在 C++ 层直接实现对应的 Module,通过 JSBinding 的机制绑定调用,省去了 C++ <-> Java 的损耗,但使用范围有限,只能局限于 SDK 内部的 Module,对于业务应用层的 Module 调用跑不通。

2)JSI

hippy-jsi

  • JSI 在 C++ 层通过 JSBinding 的 NamedPropertyHandlerConfiguration 拦截调用的对象名和方法名,转发给 HostObjectProxy 进行分发
  • 同时在 C++ 侧动态创建和持有 Java 层的 module 对象,被 TurboModuleManager 管理
  • HostObjectProxy 将 Get/Set 的属性/方法处理后分发给 HostObject,它同时持有对应的 Java Module 对象,以此实现 JS 的 同步调用

JSI 适用于调用频繁,数据对象简单,耗时较少需要同步的场景。

四、Hippy 常用组件

1、常用 Hippy 组件

以下是 Hippy 经常使用的 标签跟终端组件的映射关系:

hippy组件

2、ul 复用优化

ul 的使用是非常频繁的,重点讲下它的复用逻辑:

hippy-ul

ul 在Android 端使用的是 RecyclerView 组件,这里主要讲它的一级 Cache 缓存和三级 RecyclerPool 缓存。

  • Cache 缓存:当前刚刚滑出屏幕的缓存,默认是 2个,根据 position 查找复用,可用来上下滑动时能够快速复用,被复用后可以直接拿过来使用,无需经过 Create 和 Bind
  • RecyclerPool 缓存:缓存池的复用,根据 type 查找,每个 type 默认是5个,被复用后需要拿出来重新 Bind,即对 UI 根据数据重新刷新
  • 未命中缓存:需要重新走 Create 和 Bind 流程

如果存在刷新时闪烁的情况,可以检查是否使用到了视频组件,此时可能会由于视频组件的 Surface 销毁创建造成闪烁。在全局数据刷新时:

hippy局部刷新

由于全局刷新时是不知道哪些 item 有变更的,那么根据 position 拿屏幕上的缓存时可能由于类型不一样就会发生错误。所以它只能把屏幕上的 View 都设置为 invalid 状态,对应的 View 会被 remove 再 add,这对于带 Surface 的视频组件会监听到当前宿主发生了变化,造成销毁和创建,导致闪烁。

那么我们可以把 hasStableIds 设置为 true ,相当于强制所有 Item View 都设置了唯一的 id,这样在全局刷新时,获取屏幕上的 View 缓存,是明确知道哪些 positin 位置上的 View 完全没发生变化,就不会有 remove 和 add 的问题了。

知道是不会有人点的,但万一被感动了呢?