Skip to main content

8 posts tagged with "harmony"

View All Tags

· 9 min read
xuanzebin
TJ
heiazu
yoturg

Taro x 纯血鸿蒙

在过去的一年中,Taro 经历了显著的蜕变,Taro For Harmony 方案完成从 ArkTS 方案到 C-API 方案的升级,成功实现了对纯血鸿蒙的完全适配,扩展了 Taro 的兼容平台家族,实现了对 H5、小程序、RN、原生鸿蒙多端的统一开发。

Taro X HarmonyOS

去年 9 月,京东 APP 纯血鸿蒙 在鸿蒙应用商城正式上线,APP 中核心购物链路,如首页、搜索、商详、购物车、订单、结算和我京等页面,都是通过 Taro On Harmony C-API 版本进行开发,并且一上线就获得了华为的 S 级应用认证。

JD Harmony

今天,我们正式开源 Taro For Harmony C-API 版本,这次版本的发布,将带来更丰富的样式适配、更高效的渲染性能、更全面的组件支持,让开发者以 Web 范式的方式来开发出媲美原生鸿蒙性能的应用,为鸿蒙应用生态的丰富注入强大的动力。

JD Harmony APP

整体技术架构

Taro For Harmony 技术方案支持开发者使用 React DSL 来开发纯血鸿蒙应用,整体架构可以简单分为三层:

Taro Harmony React

最上层是应用业务代码所在的 ArkVM 层,这一层在 C-API 版本中主要运行业务代码、React 的核心代码以及少量的 Taro 运行时代码。

中间层是 Taro 的 CSSOM 和 TaroElement 树,负责处理上层 Taro 运行时代码传递下来的指令,比如 TaroElement 节点树创建,绑定关系以及设置属性等操作。

最下层存放的是 TaroRenderNode 虚拟节点树,这棵节点树和真正的上屏节点树是一一对应的关系,同时在 TaroRenderNode 节点树内会创建对应的 Yoga 节点。

Taro Harmony React DOM

同时 Taro 还基于鸿蒙提供的 VSync 机制设置一套任务处理管线,来处理中间层和下层节点树产生的样式匹配、节点测量、节点布局、样式设置以及节点上屏等任务,来保证任务的时序性和最后上屏渲染结果的正确性。

重点特性

丰富的能力支持

常用组件和 API 支持

在 C-API 版本的 Taro For Harmony 中,我们不仅完整支持了 React 18+,另外支持了 View、Text、Image、Video 等近 33 个 Taro 组件,对于常用的 API,如 getSystemInfo、getStorage 等也是在 C-API 版本中得到了完整的支持,而且针对逻辑较为复杂的 API 如:createSelectorQuery 以及 createIntersectionObserver,我们将这些 API 在 C++ 侧进行了重新的实现,大幅提升了他们的执行性能。

常用样式支持

在 C-API 版本中,我们对支持了大部分常见的 CSS 能力:

  • 支持常见的 CSS 样式和布局,支持 flex、伪类和伪元素
  • 支持常见的 CSS 定位,绝对定位、fixed 定位
  • 支持常见的 CSS 选择器和媒体查询
  • 支持常见的 CSS 单位,比如 vh、vw 以及计算属性 calc
  • 支持 CSS 变量以及安全区域等预定义变量

同时,我们参考浏览器 CSSOM 的实现方式,在 C++ 实现了一套 CSSOM 逻辑,里面包含了样式解析、样式匹配、样式合成和应用整个链路的样式处理逻辑。

Taro Harmony CSS

另外,Taro 引入了 Yoga 布局引擎来计算渲染节点的位置和大小,最大程度保证 Taro 构建出来的鸿蒙应用中渲染样式和 W3C 规范的一致性。

Taro Harmony Style

媲美原生 ArkTS 的高性能

运行时逻辑下沉至 C++

在 C-API 的版本中,我们将 ArkVM 层的 Taro 运行时内容削减到极致的薄,将 TaroElement 的大部分内容都下沉到了 C++ 侧,并在 ArkVM 层取消了他们之间父子关系的绑定,极大地提升了 TaroElement 相关逻辑的性能。

Taro Harmony CAPI

另一方面,在 C++ 侧 Taro 会指令式地调用 ArkUI 在 C++ 侧提供的 API,来高效地创建节点、设置属性、绑定事件以及绘制上屏。

提供长列表组件应对长列表场景

Taro 还针对长列表场景针对性地提供了长列表类型组件,并对长列表类型组件进行了优化,提供了懒加载、预加载和节点复用等功能,有效地解决大数据量下的性能问题,提高应用的流畅度和用户体验。

Taro Harmony Virtual List

支持 C-API 混合原生的渲染模式

Taro 的组件和 API 是以小程序作为基准来进行设计的,因此在实际的鸿蒙应用开发过程中,会出现部分所需的组件和 API 在 Taro 中不存在的情况,因为针对这种情况,在 C-API 版本中,Taro 提供了原生混合开发的能力,支持将原生页面或者原生组件混合编译到 Taro 鸿蒙项目中,支持 Taro 组件和鸿蒙原生组件在页面上的混合使用。

Taro Harmony CAPI Hybrid

使用教程

安装和使用

安装 harmony 插件

# 使用 npm 安装
$ npm i @tarojs/plugin-platform-harmony-cpp
# 使用 pnpm 安装
$ pnpm i @tarojs/plugin-platform-harmony-cpp

添加插件配置

import os from 'os'
import path from 'path'

const config = {
// ...
plugin: ['@tarojs/plugin-platform-harmony-cpp'],
harmony: {
// 当前仅支持使用 Vite 编译鸿蒙应用
compiler: 'vite',
// Note: 鸿蒙工程路径,可以参考 [鸿蒙应用创建导读](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/start-with-ets-stage-0000001477980905-V2) 创建
projectPath: path.join(os.homedir(), 'projects/my-business-project'),
// Taro 项目编译到对应鸿蒙模块名,默认为 entry
hapName: 'entry',
},
// ...
}

编译项目

# 编译鸿蒙应用
$ taro build --type harmony_cpp
# 编译鸿蒙原生组件
$ taro build native-components --type harmony_cpp

如果需要编译鸿蒙应用,同时使用编译鸿蒙原生组件,可以在页面配置中添加  entryOption: false  表示该页面是组件,同时可以用过  componentName  指定组件导出名。

export default {
navigationBarTitleText: 'Hello World',
+ componentName: 'MyComponent',
+ entryOption: false,
}

总结与展望

Taro For Harmony C-API 版本经历了京东鸿蒙 APP 的实践,综合性能、生态以及开发体验来讲,毫无疑问已经成为了开发鸿蒙应用的最佳框架选型之一。

当下,我们也仍然在不断完善着鸿蒙的适配方案,基于当前的 Taro For Harmony C-API 方案,我们会进行多线程的架构升级以及 React 的 C++ 化,进一步提升 Taro 在鸿蒙端侧的性能,并极大地降低应用的丢帧率,整体进展也已经处于验证和测试阶段。

也欢迎大家一起参与 Taro For Harmony 的共建,你们的每一个建议,每一次提交,都是推进 Taro 继续往前走最大的动力。

· 13 min read
隔壁老李
xuanzebin
TJ
heiazu
yoturg

背景

纯血鸿蒙逐渐成为全球第三大操作系统,业界也掀起了适配鸿蒙原生的浪潮,用户迁移趋势明显,京东作为国民应用,为鸿蒙用户提供完整的购物体验至关重要。

去年 9 月,京东 APP 纯血鸿蒙 在鸿蒙应用商城正式上线,APP 中核心购物链路,如首页、搜索、商详、购物车、订单、结算和我京等页面,都是通过 Taro on Harmony 方案进行开发,并且一上线就获得了华为的 S 级应用认证。

Taro 介绍

Taro 是由京东发起并维护的开放式跨端跨框架解决方案,支持以 Web 的开发范式来实现小程序、H5、原生 APP 的跨端统一开发,从 18 年开源至今,在 GitHub 已累计获得 36,000+ Stars。

在过去的一年中,Taro 完成了鸿蒙适配方案的开发,Taro on Harmony 方案支持开发者使用 React DSL 来快速开发高性能原生鸿蒙应用,让 Taro 具备了一份代码同时跨鸿蒙、小程序、H5、React Native 多端的能力,可以让开发者以熟悉的方式来开发鸿蒙应用,大幅降低鸿蒙开发门槛,并且存量的 Taro 业务也能快速转成鸿蒙原生应用,可以节约大量研发成本。

Taro on Harmony 适配方案的演进

基于 ArkTS 的初始版本

Taro 适配鸿蒙方案经过了多个版本的演进迭代,最初版本我们采用的是对接到鸿蒙原生 ArkTS 语言的方式来实现,这和 Taro 适配微信小程序的方案类似,都是通过模拟浏览器 DOM/BOM 环境,然后运行 React 代码构建出虚拟 DOM 树,再将虚拟 DOM 树以递归遍历的方式构建出宿主环境(小程序/鸿蒙)的渲染节点树,从而实现页面渲染。

但这个方案的缺陷非常明显,那就是在 ArkTS 之上再桥接一层转换,导致性能相比原生存在一定差异,并且这个差异几乎没有办法可以抹平。

基于 C API 的全新版本

恰逢此时,鸿蒙为了方便三方框架与应用接入鸿蒙生态,在官方开发语言 ArkTS 之外,开放了更底层 C API 能力,提供了 C++ 层的 UI 节点创建、属性设置等能力(类似于 C++ 版本的 DOM),三方框架和应用可以基于 C API 构建高性能的解决方案和应用。

C API 就是 Taro 想要的能力,与 Taro 的架构完美契合,Taro React 构建出来的虚拟 DOM 树可以通过 C API 直接去创建原生 UI 节点,省去了很多流程环节,并且可以将我们大部分的渲染管线处理沉到 C++ 中去,获得飞跃式的性能提升,所以基于 C API 我们首先设计并开发了单线程架构版本。

单线程架构版本

单线程架构版本的示意图如下,整个渲染过程均在主线程实现,业务代码在 ArkVM 中执行获得虚拟 DOM 树,同时读取样式数据,通过 React Reconciler ,在构建虚拟树时会调用 NAPI 在 C++ 侧构建出对应的 Element Tree,同时也会进行样式数据的解析处理,从而构建出 CSSOM 对象,Element Tree 和 CSSOM 对象会进行匹配从而构建出带有样式的 Render Tree,在这一步也会同时创建 yoga 节点,进行布局计算,最后会再基于布局计算的结果生成 C API 的 ArkNode Tree,从而实现上屏渲染。

目前京东鸿蒙 APP 中首页、搜推、我京、核心购物流程均是使用 Taro 单线程版本进行开发,性能和稳定性位于第一梯队。

多线程架构版本

单线程版本上线之后,我们也发现了单线程架构的不足,① 从业务代码的执行到渲染流程的处理都发生在主线程上,这导致了主线程的过载,使得应用无法及时响应用户的操作,从而引发了用户体验上的卡顿,随着业务逻辑的增加和复杂性的提升,这种卡顿现象会越发明显;② 在单线程架构下,代码都是需要内置到应用包里,这样就无法实现业务代码的动态下发更新。

所以,为了解决以上问题,进一步提升应用性能,和应对未来更多业务场景需求的可能性,我们实现了多线程架构。

相较于单线程架构,多线程架构将整个业务代码执行和渲染的过程拆分到了三个线程,每个线程各司其职,让任务的执行更加合理,不会造成主线程的堵塞,这样的架构主要带来三个好处。

1.业务逻辑不再堵塞主流程,完全剥离到了单独的线程执行,之前容易出现卡顿、甚至 APP Freeze 的页面基本不再有问题,例如商详接入之后滑动页面变得更加流畅;

2.动画执行和渲染不再卡顿,动画的执行交由 Background 线程管控,不再和 JS 线程有冲突问题,动画支持满帧渲染;

3.支持动态化的能力,在性能上可以与原生相媲美,同时还能支持动态下发,支持更新远端 JS 资源来实现 APP 内容的更新,让业务具备快速验证和免发版的能力。

目前多线程版本正在我们的业务中进行试点接入,很快就会正式上线。

Taro on Harmony 方案特性

丰富的能力支持

常用组件和 API 支持

在 C-API 版本的 Taro For Harmony 中,我们不仅完整支持了 React 18+,另外支持了 View、Text、Image、Video 等近 33 个 Taro 组件,对于常用的 API,如 getSystemInfo、getStorage 等也是在 C-API 版本中得到了完整的支持,而且针对逻辑较为复杂的 API 如:createSelectorQuery 以及 createIntersectionObserver,我们将这些 API 在 C++ 侧进行了重新的实现,大幅提升了他们的执行性能。

常用样式支持

在 C-API 版本中,我们对支持了大部分常见的 CSS 能力:

  • 支持常见的 CSS 样式和布局,支持 flex、伪类和伪元素
  • 支持常见的 CSS 定位,绝对定位、fixed 定位
  • 支持常见的 CSS 选择器和媒体查询
  • 支持常见的 CSS 单位,比如 vh、vw 以及计算属性 calc
  • 支持 CSS 变量以及安全区域等预定义变量

同时,我们参考浏览器 CSSOM 的实现方式,在 C++ 实现了一套 CSSOM 逻辑,里面包含了样式解析、样式匹配、样式合成和应用整个链路的样式处理逻辑。

另外,Taro 引入了 Yoga 布局引擎来计算渲染节点的位置和大小,最大程度保证 Taro 构建出来的鸿蒙应用中渲染样式和 W3C 规范的一致性。

媲美原生 ArkTS 的高性能

运行时逻辑下沉至 C++

在 C-API 的版本中,我们将 ArkVM 层的 Taro 运行时内容削减到极致的薄,将 TaroElement 的大部分内容都下沉到了 C++ 侧,并在 ArkVM 层取消了他们之间父子关系的绑定,极大地提升了 TaroElement 相关逻辑的性能。

另一方面,在 C++ 侧 Taro 会指令式地调用 ArkUI 在 C++ 侧提供的 API,来高效地创建节点、设置属性、绑定事件以及绘制上屏。

提供长列表组件应对长列表场景

Taro 还针对长列表场景针对性地提供了长列表类型组件,并对长列表类型组件进行了优化,提供了懒加载、预加载和节点复用等功能,有效地解决大数据量下的性能问题,提高应用的流畅度和用户体验。

支持 C API 混合原生的渲染模式

Taro 的组件和 API 是以小程序作为基准来进行设计的,因此在实际的鸿蒙应用开发过程中,会出现部分所需的组件和 API 在 Taro 中不存在的情况,因为针对这种情况,在 C-API 版本中,Taro 提供了原生混合开发的能力,支持将原生页面或者原生组件混合编译到 Taro 鸿蒙项目中,支持 Taro 组件和鸿蒙原生组件在页面上的混合使用。

总结

Taro 鸿蒙方案基于鸿蒙 CAPI 进行构建,实现了将 React DSL 直接对接到 C++ 侧来运行整体渲染管线,从而实现了与原生齐平的渲染性能,同时 Taro 鸿蒙方案是多线程架构的方案,在应用渲染性能、操作响应时延上都在业界做到极致,并且 Taro 鸿蒙方案支持动态更新,是行业首创且经过大规模 APP 应用场景验证的开发框架,综合性能、生态以及开发体验来讲,毫无疑问已经成为了开发鸿蒙应用的最佳框架选型之一。

目前,我们也仍然在不断完善着鸿蒙的适配方案,目前在渲染性能提升方面我们正在进行 React C++ 化的探索,整体进展也已经处于验证和测试阶段,同时也在进行自研布局引擎的探索,在进一步提升渲染性能的同时,为业务提供更丰富的样式写法支持;此外,在开发效率提升方面,我们也正在进行开发调试工具的探索开发,为业务提供热重载、源码定位、断点调试、元素审查等能力,提升效率和开发体验。

在近期,我们会将已经在京东鸿蒙 APP 久经考验的单线程架构版本开源出来,为开源社区贡献一份力量,为鸿蒙应用生态的丰富注入强大的动力。

· 15 min read
Emma
xuanzebin
TJ
heiazu
yoturg

首图

问题背景

长列表类型组件(例如 WaterFlow, List)在应用开发中被广泛使用,特别适合商品列表、订单列表、消息列表等需要无限滚动的场景。这些组件通常包含大量结构相似的子组件,它们的样式和布局相似,但数据内容各异,且数据通常来自网络。

在典型的 Taro React 应用中 ,正常的列表处理流程如下:

  1. 生成或从远端服务器加载数据。
  2. 框架基于最新数据生成新的虚拟 DOM 树。
  3. 框架使用 diff 算法或其他机制,根据虚拟 DOM 树的变化,触发对应的更新指令。
  4. 运行时捕获框架的更新请求,并实际更新视图。

然而,如果加载的数据量非常大,可能会引发严重的性能问题,导致视图在一段时间内无法响应用户操作。为了解决这个问题,我们为 Taro 长列表类型组件提供了懒加载、预加载和组件复用等功能,专门针对长列表类型组件进行了优化,有效地解决大数据量下的性能问题,提高应用的流畅度和用户体验。

Taro 虚拟列表

目前 Taro 在其他端侧已支持虚拟列表,并在实践中证明虚拟列表可以极大程度提升列表渲染性能。出于性能考虑,Taro 通过 CAPI 对接鸿蒙 ArkUI,将更多的运行时逻辑如组件、动效、测算和布局等逻辑下沉到了 C++ 层。虚拟列表也需要在 C++层重新设计实现,在只渲染当前可视区域视图的基础上,我们还加入了组件预加载,数据预请求和组件复用等高阶功能,有效地解决大数据量下的性能问题,提高应用的流畅度和用户体验,并大幅度降低了应用内存占用。以下是 Taro 虚拟列表渲染流程:

虚拟列表

优化方案

NodeAdapter 介绍

当前 ArkTS 滚动类容器使用 LazyForEach 时,框架会根据滚动容器可视区域按需创建组件,但是鸿蒙 CAPI 接口不支持 LazyForEach 功能。作为 ArkTS 侧 LazyForEach 的替代方案,开发者可以使用 NodeAdapter 对象在 C++ 侧实现类似的效果。NodeAdapter 已支持 List、Grid、Swiper 和 WaterFlow 四种主要的长列表组件。

本文将以 WaterFlow 组件为例,详细介绍如何利用 NodeAdapter 对象和 CAPI 提供的能力来实现长列表组件的懒加载、预加载和组件复用。

NodeAdapter 通过特定的事件机制时需要进行相关的处理。以下是 NodeAdapter 的典型工作流程:

NodeAdapter

通过这种方式,开发者可以更好地控制长列表组件的行为,从而显著提升其性能。

NodeAdapter 的创建和挂载如下:

  • 创建 NodeAdapter 对象并挂载回调处理函数
class RenderNode : public std::enable_shared_from_this<RenderNode> {
public:
RenderNode(std::string str) : str_(str), node_(nullptr), child_(nullptr), reuseId_("") {}
std::string str_;
std::shared_ptr<ArkUIBaseNode> node_;
std::string reuseId_;
std::shared_ptr<ArkUIBaseNode> child_;
};

ArkUIWaterFlowAdapter() : handle_(OH_ArkUI_NodeAdapter_Create()) {
// 初始化懒加载数据。
for (int32_t i = 0; i < 50; i++) {
auto renderNode = std::make_shared<RenderNode>(std::to_string(i));
renderNode->reuseId_ = "reuse";
data_.emplace_back(renderNode);
}
// 设置懒加载数据。
OH_ArkUI_NodeAdapter_SetTotalNodeCount(handle_, data_.size());
// 设置懒加载回调事件。
OH_ArkUI_NodeAdapter_RegisterEventReceiver(handle_, this, OnStaticAdapterEvent);
}
  • 将 NodeAdapter 绑定到对应的容器组件上
// 引入懒加载模块。
void SetLazyAdapter(const std::shared_ptr<ArkUIWaterFlowAdapter> &adapter) {
assert(handle_);
ArkUI_AttributeItem item{nullptr, 0, nullptr, adapter->GetHandle()};
nativeModule_->setAttribute(handle_, NODE_WATER_FLOW_NODE_ADAPTER, &item);
}
  • 回调处理函数定义
void OnAdapterEvent(ArkUI_NodeAdapterEvent *event) {
auto type = OH_ArkUI_NodeAdapterEvent_GetType(event);
auto index = OH_ArkUI_NodeAdapterEvent_GetItemIndex(event);
switch (type) {
case NODE_ADAPTER_EVENT_ON_GET_NODE_ID:
OnNewItemIdCreated(event);
break;
case NODE_ADAPTER_EVENT_ON_ADD_NODE_TO_ADAPTER:
OnNewItemAttached(event);
break;
case NODE_ADAPTER_EVENT_ON_REMOVE_NODE_FROM_ADAPTER:
OnItemDetached(event);
break;
default:
break;
}
}

懒加载

懒加载特性会根据容器组件能够容纳显示的组件数量按需加载数据,创建对应的 Item 组件,并挂载在容器组件树根组件上。整体渲染流程如下图所示(备注:图片来自 HarmonyOS NEXT 官网):

渲染流程

懒加载创建组件示例代码:

  • 创建组件唯一 ID:
// 分配ID给需要显示的Item,用于ReloadAllItems场景的元素diff。
void OnNewItemIdCreated(ArkUI_NodeAdapterEvent *event) {
auto index = OH_ArkUI_NodeAdapterEvent_GetItemIndex(event);
static std::hash<std::string> hashId = std::hash<std::string>();
auto id = hashId(std::to_string(reinterpret_cast<std::uintptr_t>(data_[index].get())));
OH_ArkUI_NodeAdapterEvent_SetNodeId(event, id);
}
  • 处理组件上屏事件:
// 需要新的Item显示在可见区域。
void OnNewItemAttached(ArkUI_NodeAdapterEvent *event) {
uint32_t index = OH_ArkUI_NodeAdapterEvent_GetItemIndex(event);
if (index >= data_.size()) {
return;
}
auto newNode = data_[index];
// RecycleManager(newNode);
// 创建新的组件元素。
auto flowItem = std::make_shared<ArkUIFlowItemNode>(0);
auto textNode = std::make_shared<ArkUITextNode>(0);
textNode->SetTextContent(newNode->str_);
textNode->SetFontSize(16);
textNode->SetPercentWidth(0.5);
textNode->SetHeight(100);
textNode->SetTextAlign(ARKUI_TEXT_ALIGNMENT_CENTER);
uint32_t color = 0xFFFF0000;
textNode->SetBackgroundColor(color);
flowItem->AddChild(textNode);
newNode->node_ = flowItem;
newNode->child_ = textNode;
if (!newNode->node_) {
return;
}
items_.insert_or_assign(newNode->node_->GetHandle(), newNode);
// 设置需要展示的组件元素。
OH_ArkUI_NodeAdapterEvent_SetItem(event, newNode->node_->GetHandle());
}

预加载

尽管按需加载列表项可以提升长列表的性能,但在用户快速滑动,特别是数据中包含大量图片和视频的情况下,可能会出现渲染不及时导致的白块现象,影响用户体验。

为了解决该问题,NodeAdapter 支持设置 cachedCount 属性来指定缓存数量,当设置 cachedCount 后,除了容器类组件内显示的 Item 外,还将预先创建 cachedCount 条数据作为缓存。这样可以有效避免白块现象,确保用户在滑动过程中有流畅的视觉体验。

一般而言,缓存的 cachedCount 为一屏显示数据的一半的时候,效果较好。

整体渲染流程如下图所示(备注:图片来自 HarmonyOS NEXT 官网):

渲染流程

  • cachedCount 的使用方法如下所示:
void setCachedCount(int32_tcount) {
ArkUI_NumberValue value[1] = {{.i32 = count}};
item = {value, 1};
nativeModule_->setAttribute(handle_, NODE_WATER_FLOW_CACHED_COUNT, &item);
}

数据预请求

尽管长列表组件支持无限滚动,但在实际业务场景中,上层业务通常会采用分页方式来加载数据。以京东 App 首页为例,一屏可以显示 6 个商品,一次网络请求加载 20 条数据。如果在滑动到底部时才发起请求加载新数据,可能会出现明显的加载停顿,尤其是在网络时延较高的情况下,这种停顿会更加突出。

为了解决该问题,我们可以调整请求新数据的时机。具体来说,当还未上屏的数据数量低于某个阈值时,就提前发起请求来加载新数据。这样可以预先准备好下一批数据,避免在滑动过程出现加载停顿。

数据预请求示例代码:

  • 注册 NODE_WATER_FLOW_ON_SCROLL_INDEX 事件,获取当前容器显示的起始位置/终止索引
void RegisterOnScrollIndex() {
nativeModule_->registerNodeEvent(handle_, NODE_WATER_FLOW_ON_SCROLL_INDEX, 0, nullptr);
}
  • 请求加载新数据
auto callback = [](ArkUI_NodeEvent*event) {
auto handle = OH_ArkUI_NodeEvent_GetNodeHandle(event);
auto eventType = OH_ArkUI_NodeEvent_GetEventType(event);
auto target = OH_ArkUI_NodeEvent_GetTargetId(event);
auto componentEvent = OH_ArkUI_NodeEvent_GetNodeComponentEvent(event);
if (eventType == NODE_WATER_FLOW_ON_SCROLL_INDEX) {
int start = componentEvent->data[0].i32;
int end = componentEvent->data[1].i32;
if (end + 10 >= waterFlow->adapter_->GetDataSize()) {
int size = waterFlow->UpdateSectionSize(20);
for (int i = 0; i < 20; i++) {
waterFlow->adapter_->InsertItem(size + i, std::to_string(size+i));
}
}
}
};

下面是使用懒加载、预加载和数据预请求的前后对比(左边为使用后,右边为使用前):

组件复用

对于离开可视区域的组件,NodeAdapter 不会主动释放它们,而是通过 NODE_ADAPTER_EVENT_ON_REMOVE_NODE_FROM_ADAPTER 回调通知开发者。这样可以让开发者根据实际情况决定是释放组件对象,还是将其放入复用缓存池以便后续使用。

Taro 采用了组件复用的策略,当组件离开可视区域时,并不会立即释放其占用的资源,而是将其放入复用缓存池中。

当用户滚动到其他位置,需要渲染新的子组件时,Taro 会先检查缓存池中是否有结构相似的组件。如果找到了相似的组件,就直接从缓存池中取出该组件,并更新其属性和事件。

这种做法可以有效降低应用的内存占用,减少频繁创建和销毁组件带来的性能开销,提升应用的整体性能和用户体验。

原理如下图所示:

组件复用

识别子组件相似性对于组件复用至关重要。由于上层业务开发者对 Item 的结构和子组件的相似性有着全面的了解, 为了更准确地识别可复用的子组件, Taro 框架引入了一个新的属性 reuseId,表示以当前组件为根组件的子组件树可以被复用。开发者可以根据业务场景和 Item 结构特点,灵活地标记出具有相似性的子组件。

当列表中存在多个具有相同 reuseId 的子组件树时, 框架会自动识别并复用这些子组件, 而不是重新创建和渲染它们。

首页组件复用

组件复用是 Taro 优化性能的重要策略,其流程如下:

  1. 从缓存池中获取与当前组件 reuseId 相匹配的子组件。

  2. 对获取到的组件进行类型比较(diff):

    • 如果组件类型相同,则认为可以直接复用该组件,进入下一步;
    • 如果组件类型不同,则销毁该组件及其所有子组件,并创建对应的新组件后挂载,结束复用流程。
  3. 对于类型相同的组件,进一步比较保其属性:

    • 通过 diff 算法识别出属性的差异部分;
    • 将差异部分的属性更新到复用的组件上,保证组件的属性与当前组件状态一致。
  4. 更新组件的事件绑定,确保事件处理函数与当前组件的事件处理逻辑相匹配。

  5. 递归地对该组件的子组件重复步骤 2 到步骤 4,实现对整个子树的深度复用。

  • 组件复用示例代码:
// 管理NodeAdapter生成的元素。
std::unordered_map<ArkUI_NodeHandle, std::shared_ptr<RenderNode>> items_;
// 管理回收复用组件池,key为 reuseId
std::unordered_map<std::string, std::unordered_set<std::shared_ptr<RenderNode>>> cachedItems_;
  • 组件回收逻辑:
// Item从可见区域移除。
void OnItemDetached(ArkUI_NodeAdapterEvent *event) {
auto item = OH_ArkUI_NodeAdapterEvent_GetRemovedNode(event);
// 放置到缓存池中进行回收复用。
auto it = items_.find(item);
if (it != items_.end()) {
auto renderNode = it->second;
if (!renderNode->reuseId_.empty()) {
cachedItems_[renderNode->reuseId_].emplace(renderNode);
}
}
}
  • 组件上屏逻辑:
bool isInitilaizedNode(std::shared_ptr<RenderNode>& newNode) {
if (newNode->node_) {
auto reuseId = newNode->reuseId_;
auto it = cachedItems_.find(reuseId);
if (it != cachedItems_.end()) {
auto setIt = it->second.find(newNode);
if (setIt != it->second.end()) {
it->second.erase(setIt);
}
}
return true;
}
return false;
}

void initRenderNode(std::shared_ptr<RenderNode>& newNode, std::shared_ptr<RenderNode>& reuseNode) {
newNode->node_ = reuseNode->node_;
newNode->child_ = reuseNode->child_;
auto newText = std::dynamic_pointer_cast<ArkUITextNode>(newNode->child_);
// 处理子组件,属性 diff 和更新
if (newText && newNode->str_ != reuseNode->str_) {
newText->SetTextContent(newNode->str_);
}
reuseNode->node_ = nullptr;
reuseNode->child_ = nullptr;
}

void RecycleManager(std::shared_ptr<RenderNode>& newNode) {
if (isInitilaizedNode(newNode)) {
return;
}
auto it = cachedItems_.find(newNode->reuseId_);
if (it != cachedItems_.end() && !it->second.empty()) {
// 组件复用
auto reuseIt = it->second.begin();
auto reuseNode = *reuseIt;
initRenderNode(newNode, reuseNode);
it->second.erase(reuseIt);
} else {
// 创建新的元素。
auto flowItem = std::make_shared<ArkUIFlowItemNode>(0);
auto textNode = std::make_shared<ArkUITextNode>(0);
textNode->SetTextContent(newNode->str_);
textNode->SetFontSize(16);
textNode->SetPercentWidth(0.5);
textNode->SetHeight(100);
textNode->SetTextAlign(ARKUI_TEXT_ALIGNMENT_CENTER);
uint32_t color = 0xFFFF0000;
textNode->SetBackgroundColor(color);
flowItem->AddChild(textNode);
newNode->node_ = flowItem;
newNode->child_ = textNode;
}
}

// 需要新的Item显示在可见区域。
void OnNewItemAttached(ArkUI_NodeAdapterEvent *event) {
uint32_t index = OH_ArkUI_NodeAdapterEvent_GetItemIndex(event);
if (index >= data_.size()) {
return;
}
auto newNode = data_[index];
RecycleManager(newNode);
if (!newNode->node_) {
return;
}
items_.insert_or_assign(newNode->node_->GetHandle(), newNode);
// 设置需要展示的元素。
OH_ArkUI_NodeAdapterEvent_SetItem(event, newNode->node_->GetHandle());
}

总结

本文重点阐述了一种针对长列表类型组件的优化方案,利用 CAPI 提供的能力,结合组件复用方案,有效地提升了页面性能。文中还附带了 CAPI 实现 demo 以供参考。目前这一方案已经在京东 App 的多个核心页面得到应用,包括首页、搜索结果页和购物车等,实践表明页面的加载和更新速度平均提升了 38%,组件复用降低京东 App 首页 54% 的内存占用。该方案在实际项目中的成功应用,为 CAPI 实现长列表组件,及长列表组件性能优化提供了有益的参考和借鉴。

下期预告

尾图

· 19 min read
HeXiao
learnNG

基于 Taro 打造的京东鸿蒙 APP 已跟随鸿蒙 Next 系统公测,本系列文章将深入解析 Taro 如何实现使用 React 开发高性能鸿蒙应用的技术内幕

背景

2024 年初,京东正式启动了鸿蒙 APP 的开发工作。由于电商 APP 大量依赖图片来展示商品信息,对图片库的性能和加载体验要求极高,因此图片库被作为核心基础能力提前纳入京东鸿蒙首期基础建设计划。本文将详细介绍京东自研鸿蒙图片库的开发过程及其技术原理。

技术实现

前期调研

经过前期的调研,我们发现HarmonyOS平台的网络图片加载主要依赖以下两种实现方式:

系统 - Image 组件

仅仅提供基础的图片组件用于图片加载,但功能、图片格式支持和扩展性都较弱,无法实现更丰富的图片库能力。存在的一些问题:

  • 性能一般,尤其在同时加载多个图片时较慢
  • 不支持 AVIF 图片格式并且无法扩展
  • 无法搭建完善的质量监控体系
  • 无法控制和扩展图片下载、解码、缓存流程,无法加入更多的性能优化手段和扩展定制
第三方库 - ImageKnife

社区参考 Android 图片库GlideHarmonyOS版本实现,使用ArkTS开发。虽然能力比系统Image组件完善,但在性能、稳定性和扩展性都无法满足诉求。存在的一些问题:

  • 性能一般
  • 代码质量一般,存在一些 bug 和 Crash
  • 整体架构、扩展性设计不够,无法加入更多的性能优化手段和扩展定制

由于系统Image组件和ImageKnife开源库无法满足诉求,我们决定自主研发鸿蒙图片库。此外,随着HarmonyOS系统的引入,客户端在原有的iOSAndroid基础设施上新增了一个平台,增加了维护成本。现有 iOS/Android 图片库也存在双端部分能力不一致的情况。集团内部期望未来的客户端基础设施能够支持跨端复用,以提高开发效率和一致性。

因此,团队选择基于C++为核心进行图片库开发,这种实现方式有以下优势:

  • 跨端复用:将更多的公共模块下沉到C++层以实现跨端复用,未来图片库可以扩展到iOSAndroid平台。
  • 适配 Taro:Taro Harmony框架基于鸿蒙CAPI进行开发,需要依赖CAPI的图片组件,可以避免ArkTSC++之间频繁通信带来的性能损耗。
  • 高性能:C++在运行时性能上优于ArkTS

JDImage 能力

架构设计

由于图片库使用C++进行跨端开发,面临不同平台之间的兼容性挑战,部分功能需要依赖于端侧的特定实现。为了应对这些复杂性,需要提供了一定的抽象能力,使得在支持不同平台时能够灵活调整和替换模块具体实现。在设计过程中也参考了iOS/Android业界成熟的开源库,如SDWebImageGlideFresco

我们选择采用模块化架构分层的思想来设计图片库的整体架构。模块化将图片库拆分为多个独立模块,不仅增强了系统的灵活性和可替换性,还使我们能够在不同的客户端环境和图片加载场景下进行灵活调整和扩展。架构分层将图片库划分为Core层和客户端层,Core层使用C++进行开发支持跨端复用,客户端层用于特定模块的平台差异化实现。这种设计方法有效地解决了跨端开发的复杂性问题,提高了系统的可维护性和稳定性。

图片库架构图

核心模块介绍

图片库主要分为图片缓存解码器图片源拉取性能监控图片组件这几个核心功能模块,模块之间不直接依赖可以被单独调用执行。

图片缓存
  • 内存缓存:用于保存已解码后的位图对象,避免每次图片加载都要进行图片解码,从而提高加载性能。我们采用LRU算法实现来高效利用内存,同时提供了最大内存缓存大小和设备内存紧张时回收一部分图片缓存的策略,以动态调整内存消耗。
  • 磁盘缓存:用于保存下载后的网络图片在 APP 沙盒中,降低网络消耗并提高加载性能。提供了基础的图片读取、保存、删除、缓存大小读取等能力,同时支持多线程并行执行,实现图片磁盘缓存的高性能读写能力。
图片数据源拉取

图片数据源拉取模块负责图片数据源读取能力。我们在HarmonyOS平台上提供了多个数据源实现,处理不同格式的图片数据源拉取任务:

  • HTTP:提供HTTP链接的网络图片下载能力。底层使用Cornet进行实现,实现高性能的多请求并行网络下载能力,同时方便未来实现跨端复用。
  • Base64:提供Base64格式的图片格式读取能力。
解码器

图片解码器模块负责将编码后的图片格式解码为操作系统的图片格式用于显示。我们在HarmonyOS平台上提供了多个解码器实现处理不同格式的图片解码任务,可以支持多线程并行执行提高解码性能。

  • 系统解码器:利用HarmonyOS系统提供的图片解码器可以支持PNGJPGGIFWebPSVG等常用图片的高性能硬件解码能力。
  • AVIF 解码器:由于京东 APP 大量使用AVIF图片格式,通过集成libavif库支持AVIF图片的解码能力。
性能监控
  • 加载性能监控:统计图片加载过程的各阶段耗时数据、缓存命中率、图片格式、资源消耗等信息,用来搭建图片加载性能数据平台和性能优化参考指标。
  • 异常监控:捕获图片加载过程中的各种错误异常,用来搭建图片加载线上异常告警和根据场景进行异常重试。
图片组件

图片组件基于系统Image组件进行封装,通过src属性传入解码后的PixelMap进行渲染。同时提供了一些额外的加载属性用于提升加载性能,例如添加lazy属性支持图片懒加载、sourceSize属性指定图片显示尺寸、 priority属性设置加载优先级。

性能优化

图片加载流水线

图片加载流水线
图片加载流水线借鉴了Fresco的流水线设计。采用流水线方式可以灵活调整流水线的执行流程,可以通过搭配不同的流水线子任务以满足不同类型的图片加载任务和不同客户端的差异化诉求。同时也添加了很多优化策略提升优化图片加载流水线的执行性能。图片加载流水线主要包含以下这些能力:

  • 调度执行顺序:管理图片加载的流水线子任务执行顺序,确保各个步骤按照预期的顺序进行执行。
  • 调用功能模块:灵活调用不同的功能模块,如解码、缓存、网络请求等,根据具体需求动态调整,以适应不同的图片加载任务。
  • 线程调度:对流水线子任务进行执行线程调度,确保非主线程任务在子线程中并行执行,主线程任务在主线程中执行。使用HarmonyOS提供的FFRT框架进行高性能线程调度。
  • 重复任务聚合:识别重复的加载任务,避免非必要的重复请求,提高加载性能。
  • 加载监控:统计加载过程性能数据和错误异常。同时加载数据也可以用于回溯分析异常问题。
  • 取消/重试机制:实现图片加载的取消和重试逻辑。提前取消可以降低非必要的图片加载请求,重试机制可以尽可能降低线上显示异常。
面向京东图片服务器设计

京东图片服务器提供了多种处理功能,例如图片格式转换图片降质图片缩放图片圆角灰度图等。这些功能通过在图片URL中添加特定图片处理参数实现,图片服务器会根据参数设置提前将图片处理完成并保存到CDN服务器。

我们可以通过在流水线预处理阶段,针对京东域名图片自动追加特定的图片处理参数降低网络流量传输消耗,例如根据视图实际显示大小添加缩放参数,添加降质参数,使用.avif格式图片降低网络传输大小。同时在磁盘缓存模块实现中,我们也通过实现优化策略让部分相同的图片可以进行磁盘缓存复用避免重复下载。

URL预处理

质量保障

作为一个新上线的基础组件,保证业务稳定性至关重要。为了快速发现并解决问题,图片库中实现了线上监控和异常恢复机制。

线上监控

####### 异常监控

如前文所述,图片加载流水线涉及多个子任务,加载异常可能出现在任何一个环节。为了精准定位问题,图片库为每个子任务定制了不同的异常对象,每个异常对象包含独特的错误码,并会根据严重程度将其分为异常和警告两个级别。当加载流程结束时会根据上报策略进行异常上报。

性能监控

除了加载成功率,加载速度也是图片库的核心指标,为了能正确还原加载流程,图片库按照配置策略记录每个子任务开始和结束时间,一方面用于统计各阶段的加载耗时,另一方面还作为异常定位时的重要辅助信息。

异常恢复

除了从监控发现问题,为了将异常影响降至最低,图片库也实现了流水线失败重试、组件异常降级及线上配置降级三种异常恢复机制。

流水线失败重试

在图片库任务预处理阶段,会基于配置策略对图片链接进行处理,如统一图片域名,图片格式转换等等。由于图片链接拼接规则复杂,无法保证所有链接进行转化处理后仍能正常加载,因此图片库流水线中增加了重试机制,会根据错误类型使用不同的重试策略。

组件异常降级

对于经过重试仍然失败的任务,图片库组件侧会尝试直接使用系统Image组件进行兜底渲染,以防止出现图片库特定格式兼容性的问题。
此外,因为图片库流水线加载完成不意味着图片一定能正常渲染,图片的最终渲染成功与否取决于最终图片是否能在图片组件完成渲染上屏。因此,针对流水线成功但组件渲染失败的情况,图片库也会尝试降级到系统Image组件加载原始链接。
如果降级后仍然失败,则会按照业务配置顺序加载异常兜底图。

线上配置降级

由于线上环境复杂,可能会出现仅发生在特定用户、系统版本、APP 版本的异常,图片库也基于移动配置 SDK 支持了针对特定用户、机型、版本强制降级的能力。

加载对比

加载对比图

未来规划

我们也梳理了未来可能尝试的一些图片库优化方向:

  • 网络优化:弱网优化、HTTPDNS 等策略优化图片网络下载性能。
  • 性能优化:提高磁盘缓存/内存缓存复用率、使用堆内存池减少堆内存分配、优化 GIF 播放、低端机优化等。
  • 适配京东图片服务器能力:利用服务器图片处理能力,如使用不同的缩放尺寸参数、清晰度、图片格式、图片域名等参数提高加载性能。
  • 能力完善:提供更多图片加载参数、支持更多图片类型等能力满足不同业务的加载诉求。
  • 更多平台支持:扩展到iOSAndroid等更多平台。
  • 提升图片效果:使用不同的插值方式、超分技术、HDR 等能力提高图片显示效果。

总结

京东自研的鸿蒙图片库通过模块化设计和流水线调度策略,成功解决了跨端开发的复杂性问题,同时提高了系统的可维护性和稳定性。通过实现多重异常容错机制,保证了业务的稳定性。该图片库不仅满足了京东 APP 在鸿蒙系统上的高性能图片加载需求,还为未来的跨平台开发奠定了基础。

目前,图片库已通过Taro集成到京东鸿蒙 APP 中上线使用,在图片加载体验和稳定性上有不错的表现。未来,我们将继续优化图片库的性能,扩展功能,并探索在更多平台上的应用可能。同时,我们也会持续关注业界最新的图片处理技术,不断改进我们的解决方案,为用户提供更好的图片加载体验。

系列往期精选

下期预告

下期预告

· 13 min read
TJ
xuanzebin
heiazu
yoturg

title_logo

基于 Taro 打造的京东鸿蒙 APP 已跟随鸿蒙 Next 系统公测,本系列文章将深入解析 Taro 如何实现使用 React 开发高性能鸿蒙应用的技术内幕

背景

在鸿蒙生态系统中,虽然原生应用通常基于 ArkTS 实现,但在实际研发过程中发现,使用 C++ 可以显著提升应用框架和业务的性能表现。随着鸿蒙系统的不断迭代升级,不同语言环境间的协作已成为不可或缺的开发范式,共同构建了更丰富的研发生态。

Taro 通过接入鸿蒙端的 C-API 相关能力,将组件、样式布局等运行时逻辑下沉到 C++ 层,从而极大地提升了页面的渲染性能。

在这样的背景下,构建一套在 C++、ArkTS 等不同语言环境之间高效通信的事件系统,成为了一个极具价值,对于 Taro 来说也是必修的课题。

多语言环境的事件处理机制

在 Harmony 端的适配过程中,事件系统扮演着双重角色:不仅驱动应用、页面和各模块组件的生命周期,还因为 ArkTS 和业务代码(JS)之间存在人为设定的界限,需要事件作为桥梁,以便 JS 能够调用 ArkTS 的原生能力。

跨语言环境事件驱动架构的设计考量

在设计跨语言环境的事件驱动架构时,需要同时考虑 ArkTS、JS 和 C++ 等多个语言环境的限制和运行时差异。如何实现事件在这些环境之间的有序传递,以驱动页面和组件的生命周期,是事件系统设计的重要考量。

mindmap

通过 C++ 实现事件的底层逻辑,构建一个高效的事件管理系统,可以有效避免冗余接口的设计。同时,与鸿蒙的 C-API 支持的事件系统对接,将各类事件分发到不同语言环境,确保跨语言环境的事件分发与处理的有序性、高效性。

roadmap

回顾 Taro 开始适配鸿蒙至今,事件系统也随之经历了从简单到完善的演进历程。从最初在 ArkTS 方案中的基础实现,到随着 Taro for Harmony 方案迭代发展,事件系统的设计也面临 ArkTS 带来的一些限制。

在 ArkTS 语言环境中事件架构的局限性

基于 ArkTS 语言环境实现的事件架构,在性能方面存在较大局限性。特别是在事件冒泡过程中,性能较差的语法,和回调逻辑可能会导致性能严重劣化,甚至阻塞主线程。这不仅会影响应用的响应速度,更有甚者可能对整体用户体验产生负面影响。

single_thread

为了解决这些问题,提升性能以保证用户体验成为关键目标。通过将事件处理逻辑下沉到 C++ 层,并置于后台线程执行等优化手段。能够有效提高代码执行效率,同时避免逻辑阻塞主线程导致的延迟响应,以提升应用的流畅性,提供更佳的用户体验。

mutli_thread

构建多语言环境下的事件系统

在构建多语言环境下的事件系统时,首要考虑各种类型的事件,比如:鸿蒙提供的组件通用事件手势等。事件系统需要有效地管理这些不同的事件来源,并根据框架和用户的监听行为有序进行事件的分发。

events

在这些事件类型中,大致可以分为普通事件和节点事件两类。前者涵盖系统层面和应用、组件等生命周期的变化,通常由系统或应用状态的改变触发,主要由事件中心(eventCenter)来处理;节点事件则与 DOM Tree 紧密相关,这些事件通常需要快速响应,以确保用户界面的流畅性和交互的即时性。

事件中心(eventCenter)的实现

作为 Taro 运行时中的基础模块,事件中心专注于处理系统事件和生命周期。它允许框架和应用开发者在后台线程注册事件队列,并异步分发事件,从而有效减轻主线程的负担。事件中心能够快速响应各种事件,同时具备健壮的错误处理机制,帮助开发者快速定位和解决事件回调中的问题,从而提升开发效率和系统稳定性。

事件监听与分发

开发者可以在 C++ 和 ArkTS 等多种语言环境中创建事件监听器,并将相应的回调函数添加到事件队列中。这一机制允许开发者在不同的编程语言中灵活地定义和处理事件响应逻辑。

当事件触发时,会根据不同语言环境的运行时差异,将事件参数转换为对应的格式。这种参数转换确保了各语言环境能够正确理解并处理事件及包含的数据,无论是简单的数据类型还是复杂的对象结构,都能在不同语言之间无缝传递。

event_center

事件队列会根据监听器的类型,按照预定义的顺序,将事件分发到相应的语言环境中。这样一来,每个监听器都能在其所属的环境中高效地执行对应的回调函数。通过这种方式,不仅可以实现了跨语言的事件处理,优化事件的分发效率,并确保应用在响应用户交互时保持高性能和高稳定性。

需要注意的是,受限于底层限制,在 ArkTS 环境中注册的事件需要回到主线程执行,同时在鸿蒙端不支持 Symbol 类型的事件。

节点事件处理(domEvent)

在 HTML 中,节点事件处理流程会如下图所示,事件从根节点开始向下传播至目标节点,触发后再从目标节点顺着节点树向上冒泡。在鸿蒙端实现中,Taro 基于这一事件传播流程,为开发者提供一致的事件处理机制。

事件类型

在 Taro 框架中,节点主要处理三种类型的事件:鸿蒙事件、鸿蒙手势事件和自定义事件。这些事件都是从  TaroElement  上进行监听和触发的。根据事件的类型不同,节点会从相应的事件源设置  Receiver (事件接收器)来进行监听并处理回调逻辑。

event_dom

鸿蒙事件和鸿蒙手势事件分别通过  RenderNode  注册到  Receiver,确保事件能够正确地传递和触发。而自定义事件则根据节点实现或用户自行触发,以满足各种不同类型的交互响应。

事件传播

当  TaroElement  上的事件被触发后,事件会沿着节点树向上传播。每个节点依次接收到事件,并执行相应的回调。执行完回调后,会检查开发者是否阻止冒泡,以决定是否继续向上传播。事件从目标节点开始,逐级往上直到根节点或者冒泡被阻止。

html_event 这允许开发者在事件传播过程中,通过任意节点处理或拦截事件来调整业务逻辑实现,以更灵活的方式在特定节点上执行逻辑,或通过阻止冒泡避免对上层节点的影响。这样的设计对于前端开发者来说,更加熟悉、直观。

鸿蒙系统的底层节点事件也有自己的传播逻辑,但由于其机制与 ArkNode 节点树差异,为避免其事件干扰,需要阻止其冒泡行为并接管其传播流程,以确保事件传播与节点树正确关联。

事件回调

由于节点事件也需要回调 JS 环境中执行,根据事件类型的不同,按照 Web 标准将相应的节点、值和方法如 targetstopPropagationvalue 等等挂载到事件对象上。通过执行当前回调的序列化方法,确保事件在不同语言环境传递时,可以保证其回调对象能力一致、参数完整。

在 C++ 中,许多组件依赖于事件机制来实现功能。例如,通过鸿蒙事件更新组件属性,还有各个组件节点间的事件传递等。这些组件利用事件机制来确保数据变化能够及时反映,并且用户交互能够顺利传递到系统的各个部分。

总结与展望

在多语言环境中,确保事件在不同语言环境传递时的一致性尤为重要,各个模块以及应用内不同页面或组件通过事件解耦驱动来提升可维护性。当前的解决方案有效提升了系统的响应速度和模块间的协作能力。

当下方案实现中仍然存在一些问题,比如早期通过事件绕过 ArkTS 与 JS 之间相互调用限制等场景,可以通过 TurboModule 来提供更加直接的调用方案。

未来,在 Taro for Harmony 场景下,各语言模块的协同将进一步增强。基于事件系统的设计,可以有效地解耦模块间逻辑,实现更灵活的组合。

拓展阅读

下期预告

next

· 17 min read
yoturg
xuanzebin
TJ
heiazu

基于 Taro 打造的京东鸿蒙 APP 已跟随鸿蒙 Next 系统公测,本系列文章将深入解析 Taro 如何实现使用 React 开发高性能鸿蒙应用的技术内幕

背景

HarmonyOS 采用自研的 ArkUI 框架作为原生 UI 开发方案,这套方案有完善的布局系统和样式控制,但是他的标准与 W3C 的 CSS 标准存在不一致性。这意味着,如果 Taro 直接使用 HarmonyOS 提供的样式系统,开发者在使用 Taro 开发时会遇到非常多的样式兼容性问题,写出来的代码也会失去跨平台兼容的能力,与 Taro 多端统一开发的定位不符。如何抹平 ArkUI 标准和 W3C 的 CSS 标准之间的差异成了一个重中之重的任务。

本文将介绍 Taro 处理 CSS 的全流程,剖析将不同的 CSS 样式转换为 ArkUI 样式遇到的问题和对应的解决方案。

CSS 样式和 ArkUI CAPI 样式的差异和抹平

1、样式书写方式不一致

以几个我们日常会使用到的属性为例,下面的分别是 CSS 的写法和 ArkUI CAPI 的写法。

对比可以看出,CSS 样式和鸿蒙样式在单位系统和数据表示方式上存在显著差异。CSS 提供多样化的尺寸和颜色单位,而 ArkUI 的 CAPI 接口采用更统一的表示方式。

ArkUI 的 CAPI 接口将所有尺寸统一为 vp 单位,颜色采用 0xAARRGGBB 格式的 uint32 类型,对于渐变和 transform 等复杂样式属性,更是需要转换为颜色停止点和角度值列表和矩阵运算,这样的接口简洁但需要调用者根据具体场景完成必要的单位转换。

一个页面通常会包含非常多的样式规则,如果所有的单位转换都放在运行时候完成,必定会造成明显的性能问题。因此,我们选择提前完成部分转换。HTML 节点的样式主要来源于 CSS 和 Style 属性。CSS 样式通常是静态的,可以在编译阶段进行转换。为此,我们基于 lightningCSS 开发了一个 Rust 插件。该插件通过遍历项目 CSS 的抽象语法树(AST),将其转换为 ArkUI 的 CAPI 接口可直接使用的数据结构。

而对于 Style 属性,其内容在运行时才能确定,因此必须在运行时进行转换。在 React 的语法中,Style 可能以字符串形式呈现,也可能是 CSS 属性名和属性值的键值对。为了有效解析 Style,我们针对各种类型的 CSS 语法写了一系列小型的 CSS 语法解析逻辑。这些逻辑能够从各种不同格式的字符串中准确匹配出属性值并进行转换。

虽然这种方法需要在运行时进行语法分析,但考虑到 Style 属性通常只包含有限的样式,加上 C++语言的高效执行特性,这种实时转换对性能的影响可以忽略不计。更为重要的是,这些运行时的 CSS 语法解析逻辑可以为后面 Taro 支持 CSS 变量提供能力支持。

2、布局存在差异

除了书写方式的差异,ArkUI 有很多布局属性的行为在细节上也和 W3C 的布局属性存在着不小的差异,比如鸿蒙的绝对定位相对父级定位,Web 的绝对定位相对最近的已定位祖先元素定位,并且鸿蒙的定位不支持 right 和 bottom(早期)。

web 可以通过 margin 的 auto 实现居中,鸿蒙能通过 flex 实现居中。

同时, ArkUI 的 CAPI 接口缺少一些 Web 常用功能,如 calc() 计算和百分比设置支持。为消除这些差异,我们选择采用 Yoga 作为布局引擎,而非使用鸿蒙原生提供的布局。Yoga 是 Facebook 开发的开源跨平台布局引擎,实现了基于 Web 标准的 FlexBox 布局算法。使用 Yoga 可以很容易地实现对大部分 CSS 布局属性的支持,让两端的差异缩小 。

在具体实现中,我们需要在构建 Taro 节点树的同时,构建结构一致的 Yoga 节点树。

然后把原本直接设置到鸿蒙节点上的样式属性(如宽高、margin、padding、display 和 position)设置到 Yoga 节点上。经 Yoga 计算后,我们再从 Yoga 节点上读取计算后的 width、height、x 和 y 值设置到鸿蒙节点上,从而实现鸿蒙端和 web 端的布局一致性。

通过使用 Yoga 作为布局引擎,我们不仅解决了鸿蒙系统与 Web 布局之间的差异,还提高了跨平台一致性。这种方法使开发者可以使用熟悉的 Web 布局概念,同时确保在鸿蒙平台上获得预期的布局效果。

样式的工作流程

介绍完 Taro 适配 ArkUI 的 CAPI 样式过程中遇到的问题和对应的解决策略之后,我们就可以来看看基于这些策略,鸿蒙样式的整个工作流程是怎么样的。

样式初始化

首先,项目启动后,编译器处理后的样式文件将第一个被加载到运行时环境。样式处理逻辑会根据各个选择器(selector)生成相应的样式规则(StyleRule),即 CSS 属性的键值对集合。

根据 ClassName 匹配 StyleRule

React 在构建每个节点的同时,会通过 Reconciler 把 React 节点的 ClassName 和 Style 设置到相应的 Taro 节点上,这个时候我们就开始进入节点的样式匹配环节。样式处理会执行以下步骤:首先,从 CSSStylesheet 这个样式集合里识别出与 className 相关的所有 StyleRule;然后,根据选择器的优先级合并这些 StyleRule;最后,将合并结果与由 Style 生成的 StyleRule 合并,从而得出最终的样式配置。

这里顺带提一下,CSS 除了可以书写样式之外,还可以书写伪元素和关键帧动画,这两者在都没办法直接设置到鸿蒙的样式里,在处理某个节点时,如果匹配到这个节点的样式里包含伪元素,就会把这个伪元素转换成 一次 insertBefore api 的调用,用这个新 insert 进去的子元素来承载伪元素的 StyleRule,举一个例子,下面的 F 节点的 CSS 样式里带有一个 ::after 的伪类,那么当 F 节点匹配到这个样式的时候,就会被插入一个子节点用来承载 ::after 对应的样式。

而对于匹配到关键帧的动画,会把动画对应的元素,动画播放的次数、播放的方向、播放的缓动函数收集起来放到另外的线程,由这个线程算出元素每一帧对应的属性值,并在元素当前帧的 StyleRule 设置完之后,设置到节点上,保证动画的优先级一定是最高的。

样式的应用

确定了节点对应的样式表后,我们就到了把样式应用到节点上这个环节了。这个环节我们会调用节点的 SetStyle 方法,遍历 StyleRule 中的所有样式。对于布局相关的属性(如 display、position、float、flex、width、height、margin、padding),如需更新,会被设置到节点对应的 Yoga 节点上,同时为节点本身添加 layout_dirty 标记。接着,判断是否有绘制相关的属性需要更新,如果有,则设置到节点对象的临时属性上,并为节点添加 draw_dirty 标记。这些被标记的节点并不会立刻被处理,而是会被纳入下一帧的样式处理队列中,这样能避免同一帧多次设置同一个结点的相同属性,确保样式更新的高效性,同时也能保证布局属性和绘制属性设置到鸿蒙节点时的前后时序。

在标记完所有需要更新的节点后,下一帧的样式处理流程就会对这些节点进行处理。首先,系统会调用 Yoga 的 calcYGLayout 函数,让 Yoga 从根节点开始对所有的 Yoga 节点进行测算。在此过程中,布局信息发生改变的 Yoga 节点会被打上 has_new_layout 的标记,节点上的信息也会被更新。

我们用一个例子来说明 Yoga 如何判断布局变化的影响范围:假设节点 E 的宽度改变,这可能影响到依赖父元素宽度的子元素以及由子元素撑开宽度的父元素。计算后,系统可能会更新 A、B、C、E、F、H 等节点。

测算完成后,我们遍历 Yoga 节点树,找出标记为 has_new_layout 的节点,并将其 width、height、x、y 值更新到对应的鸿蒙节点上。这样,所有节点的布局信息就更新完毕了。

布局更新完成后,我们再把前一帧中添加到样式处理队列的节点拿出来。将存储在节点临时对象中的绘制属性转移到鸿蒙节点上。在这个环节里大多数绘制属性可以直接设置,少量依赖节点布局信息的属性(如百分比形式的 background-size)也可以利用新计算出的布局信息来准确确定这些属性的值。

样式的更新

了解了初始化状态的样式工作流程后,我们再回过头来看一下样式更新部分的逻辑,在这一块逻辑里,样式的匹配和应用与前面的流程没有任何区别,所以只是简单介绍一下一个节点的样式是怎么被更新的。

Style 更新

Style 的更新是相对比较好处理的一部分,因为 Style 的影响范围只在节点的本身。当元素的 Style 更新时,我们只需要重新生成对应的 inline_style_,然后将其与通过 className 生成的样式进行合并应用即可。这个过程相对简单直接,因为不需要考虑对其他元素的影响。通过这种方式,我们可以确保元素的样式得到准确更新,同时保持整体样式系统的一致性和效率。

ClassName 更新

当元素的 ClassName 更新时,我们需要执行以下步骤来确保样式正确应用:

1.识别包含新 ClassName 的所有选择器规则。

2.根据 ClassName 在规则中的位置,确定需要重新进行样式匹配的元素:

  • 目标元素选择器:更新当前节点
  • 直接后代选择器:更新直接子节点
  • 后代选择器:更新所有子孙节点

这些规则适用于 className 的增加、删除、修改和查询操作。对于 className 的修改,我们将其视为先删除旧 className 再添加新 className,并执行两次规则匹配。

举一个例子,当样式规则和元素结构如下时:

.E .G {}
.E .H {}
.I > .J {}
.I {}

为蓝色的节点添加 className I,为红色的节点添加 className E,那么需要要被更新的节点就有 F G H I J

在实际应用中,我们还需考虑性能问题。对于大型应用或复杂的元素结构,频繁的样式重计算可能会影响性能。因此,我们采取了一种优化策略:找出需要更新的节点后,不会立即进行样式重匹配,而是将这些节点标记为"脏"并放入更新队列中。然后,我们在下一帧统一完成所有样式重匹配的工作。这种方法可以有效减少重复计算,提高整体性能。

总结

通过本文,我们详细阐述了 Taro 在处理 CSS 样式与鸿蒙系统 ArkUI 框架之间差异的全流程。我们探讨了样式书写方式的不一致性、样式匹配和应用的复杂过程,以及样式更新时的处理策略。这些功能和特性使得 Taro 能够在保持跨平台兼容性的同时,实现 CSS 样式到鸿蒙系统的有效转换。

作为开发者,我们深知这个过程中面临的挑战,但也为最终取得的成果感到自豪。通过这种方法,我们为开发者提供了一个统一且强大的多端开发解决方案,使他们能够更加高效地开发跨平台应用。

我们相信,随着技术的不断进步,未来还会出现更多的优化空间。我们将继续致力于改进 Taro 的性能和兼容性,为开发者提供更好的开发体验。同时,我们也欢迎社区的反馈和贡献,共同推动 Taro 在多端开发领域的发展。

系列往期精选

《京东鸿蒙上线前瞻——使用 Taro 打造高性能原生应用》

Taro 鸿蒙技术内幕系列(一):如何将 React 代码跑在 ArkUI 上文章

《Taro 鸿蒙技术内幕系列(三) - 多语言场景下的通用事件系统设计》

· 11 min read
heiazu
xuanzebin
TJ
yoturg

基于 Taro 打造的京东鸿蒙 APP 已跟随鸿蒙 Next 系统公测,本系列文章将深入解析 Taro 如何实现使用 React 开发高性能鸿蒙应用的技术内幕

背景

随着鸿蒙操作系统的快速发展,开发者们期待将现有跨平台应用迁移到鸿蒙平台。Taro 作为一个流行的跨平台开发框架,其支持鸿蒙系统的可能性引起了广泛关注。

然而,鸿蒙系统采用全新的 ArkUI 框架作为原生 UI 开发方案,与 Taro 原本支持的平台存在显著差异。将 Taro 的 React 开发模式与 ArkUI 的声明式 UI 开发范式进行有效对接成为了一个技术难题。

本文将探讨 Taro 框架如何通过创新方案实现 React 代码在 ArkUI 上的运行。我们将解析 Taro 的运行时原理,剖析其如何将 React 组件转换为 ArkUI 可识别的结构,以及相关技术挑战和解决方案。

Taro 运行时原理介绍

为了理解 Taro 适配 ArkUI 的核心机制,我们首先需要深入了解 Taro 的运行时原理。Taro 通过巧妙的设计,将 React 代码转换为各平台可执行的形式,其中包括对鸿蒙平台的适配。下面将详细介绍 Taro 是如何将 React 代码转换为 ArkUI 可执行的形式,以及节点转换的流程细节。

1. 从 React 到 Taro

React 跨平台的秘诀

在 Taro 的运行时中,首先执行的是开发者编写的 React 业务代码。这些代码定义了业务应用的结构、逻辑和状态管理。那么既然要对接 React,那肯定先得了解它的核心架构,React 是怎么运作的:

了解了 React 的基本架构后,我们可以清晰地看到,**Renderer** 作为渲染器,负责将 React 的虚拟节点操作最终映射到相应的平台上。例如,react-dom将这些操作对接到浏览器上,而react-native则将其对接到 iOS 或 Android 平台。这种设计使得 React 能够适配不同的运行环境。

正是基于这种思路,Taro 团队设计了 Taro Renderer。这个渲染器充当了 React 与 Taro 虚拟节点树之间的桥梁,使得 React 的操作可以被转换为 Taro 的中间表示。

通过实现 Taro Renderer 生成 Taro 虚拟节点树

HostConfig 接口实现

要实现 Taro 的 Renderer,我们需要实现 React Reconciler 所需的 hostConfig 接口。这个接口定义了一系列方法,用于创建、更新和管理渲染目标平台的元素。以下是一些关键的 hostConfig 方法:

  • createElement:创建 ArkUI 对应的元素。
  • createTextInstance:创建文本节点。
  • appendChild:将子元素添加到父元素。
  • removeChild:从父元素中移除子元素。
  • insertBefore:在指定位置插入元素。
  • commitUpdate:更新元素属性。

通过实现这些方法,Taro Renderer 能够将 React 的操作转换为 Taro 虚拟节点树的相应操作。这个虚拟节点树是 Taro 实现跨平台的核心,它为不同平台的渲染提供了统一的中间表示。

// 部分HostConfig接口实现的代码

const hostConfig: HostConfig {
// 创建Taro虚拟节点
createInstance (type, props: Props, _rootContainerInstance, _hostContext, internalInstanceHandle: Fiber) {
const element: TaroElement = TaroNativeModule.createTaroNode(type)
precacheFiberNode(internalInstanceHandle, element)
updateFiberProps(element, props)
return element
},
// 更新属性
commitUpdate (dom, updatePayload, _, oldProps, newProps) {
updatePropsByPayload(dom, oldProps, updatePayload)
updateFiberProps(dom, newProps)
},
// 插入节点
insertBefore (parent: TaroElement, child: TaroElement, refChild: TaroElement) {
parent.insertBefore(child, refChild)
},
// 移除节点
removeChild (parent: TaroElement, child TaroElement) {
parent.removeChild(child)
},
// ...
}

2. 从 Taro 到 ArkUI

在将 Taro 虚拟节点树转换为 ArkUI 的过程中,我们需要进行几个关键步骤:

Taro Element 转换 ArkUI 过程

首先,我们需要在 ArkUI 层面实现一套与 Taro 组件对应的组件库。这个步骤至关重要,因为它建立了 Taro 组件和 ArkUI 组件之间的映射关系。例如,我们需要为 Taro 的 ViewTextImage 等基础组件创建对应的 ArkUI 组件。这样,当我们遍历 Taro 虚拟节点树时,就能找到每个节点在 ArkUI 中的对应实现。

在节点映射的过程中,我们注意到 Taro 虚拟节点树与实际 ArkUI 视图结构存在差异。这些差异主要体现在以下几个方面:

  • 复合组件结构:某些 Taro 组件在 ArkUI 中可能需要多个组件配合实现。例如,ScrollView 组件在 ArkUI 中可能需要一个 Scroll 节点搭配一个 Stack 来实现完整功能。
  • 层级位置调整:一些特殊定位的节点(如 Fixed 定位的元素)在最终渲染时的位置可能与其在虚拟节点树中的层级不一致。这需要在生成渲染树时进行特殊处理。
  • 平台特定组件:某些 Taro 组件可能需要使用 ArkUI 特有的组件或布局方式来实现,这要求我们在转换过程中进行适当的调整和映射。

因此,在生成渲染树时,我们需要一个更复杂的转换过程,不仅要考虑简单的一对一映射,还要处理这些结构性的差异,确保最终生成的 ArkUI 组件树能够正确反映预期的视图结构和布局。因此,在 Taro > ArkUI 的节点对接中,我们需要维护一棵 Render Tree,用于做中间的桥梁。

1. 根据组件类型 创建 Taro Element

在创建 Taro Element 的过程中,我们根据组件的类型来实例化相应的 Taro 元素。这一步骤是将 React 组件转换为 Taro 内部表示的关键。

// 根据组件类型创建对应的Taro节点
std::shared_ptr<TaroElement> TaroDocument::CreateElement(napi_value &node) {
// 获取组件类型
TAG_NAME tag_name_ = TaroDOM::TaroElement::GetTagName(node);
// 根据组件类型,创建对应的实例
std::shared_ptr<TaroDOM::TaroElement> item;
switch (tag_name_) {
case TAG_NAME::SCROLL_VIEW: {
item = std::make_shared<TaroDOM::TaroScrollView>(node);
break;
}
case TAG_NAME::IMAGE:
item = std::make_shared<TaroDOM::TaroImage>(node);
break;
}
case TAG_NAME::SPAN:
case TAG_NAME::TEXT: {
item = std::make_shared<TaroDOM::TaroText>(node);
break;
}
case TAG_NAME::SWIPER: {
item = std::make_shared<TaroDOM::TaroSwiper>(node);
break;
}
// ...
}
return item;
}

2. Taro Element 创建 Taro RenderNode

在创建完 Taro Element 之后,下一步是将其转换为 Taro RenderNode。这个过程是将 Taro 的内部表示进一步转化为更接近 ArkUI 结构的渲染节点。

// 创建 Taro RenderNode
void TaroSwiper::Build() {
if (!is_init_) {
// create render node
TaroElementRef element = std::static_pointer_cast<TaroElement>(shared_from_this());
auto render_swiper = std::make_shared<TaroSwiperNode>(element);
render_swiper->Build();
}
}

3. Taro RenderNode 创建 ArkUI Node

最后一步是将 Taro RenderNode 转换为实际的 ArkUI 节点。这个过程涉及到直接与 ArkUI 的底层 API 交互,创建和配置 ArkUI 的原生节点。实现了从 Taro 的渲染节点到 ArkUI 实际可渲染节点的最终转换。

  // 创建 ArkUI Node
void TaroSwiperNode::Build() {
NativeNodeApi *nativeNodeApi = NativeNodeApi::getInstance();
// 创建一个Swiper的ArkUI节点
SetArkUINodeHandle(nativeNodeApi->createNode(ARKUI_NODE_SWIPER));
}

通过这三个步骤,我们在 C++ 层面成功实现了 React 组件结构到 ArkUI 原生组件结构的映射。这一过程使 Taro 应用能够在鸿蒙系统上准确地渲染和运行,为跨平台开发提供了有力支持。

总结

最后总结下,本文探讨了 Taro 框架如何将 React 代码成功运行在鸿蒙系统的 ArkUI 上。这个过程主要分为两个关键部分:

React > ArkUI 架构图

1. Taro 对接 React

Taro 通过实现自定义的 Renderer 来对接 React。这个 Renderer 包含了一系列方法,如 createInstance、commitUpdate 等,用于将 React 的操作转换为 Taro 虚拟节点树的操作。这个虚拟节点树是 Taro 实现跨平台的核心,为不同平台的渲染提供了统一的中间表示。

2. Taro 对接 ArkUI

Taro 通过自定义 Renderer 将 React 操作转换为虚拟节点树,然后通过三步转换过程将其映射到 ArkUI 结构。这个过程涉及 Taro Element、Taro RenderNode 和 ArkUI Node 这三棵树的维护,主要通过这三个流程步骤实现:

1.创建 Taro Element:这一步将 React 组件转换为 Taro 内部表示。

2.创建 Taro RenderNode:将 Taro 的内部表示进一步转化为更接近 ArkUI 层级结构的渲染节点。

3.创建 ArkUI Node:最后一步是将 Taro RenderNode 转换为实际的 ArkUI 节点,直接与 ArkUI 的底层 API 交互。

通过这种方式,Taro 成功地将 React 组件结构映射到 ArkUI 原生组件结构,使得 Taro 应用能够在鸿蒙系统上准确地渲染和运行,同时也为跨平台开发提供了有力支持。

系列往期精选

《京东鸿蒙上线前瞻——使用 Taro 打造高性能原生应用》

· 13 min read
xuanzebin
TJ
heiazu
yoturg

背景

2024 年 1 月,京东正式启动鸿蒙原生应用开发,基于 HarmonyOS NEXT 的全场景、原生智能、原生安全等优势特性,为消费者打造更流畅、更智能、更安全的购物体验。同年 6 月,京东鸿蒙原生应用尝鲜版上架华为应用市场,计划 9 月完成正式版的上架。

配图2.png

早在 2020 年,京东与华为就签署了战略合作协议,不断加大技术投入探索 HarmonyOS 的创新特性。作为华为鸿蒙生态的首批头部合作伙伴,在适配鸿蒙操作系统的过程中,京东与华为一直保持着密切的技术沟通与共创,双方共同攻坚行业适配难点,并推动多端统一开发解决方案 Taro 在业界率先实现对鸿蒙 ArkUI 的原生开发支持。

本文将阐述京东鸿蒙原生应用在开发时所采用的技术方案、技术特点、性能表现以及未来的优化计划。通过介绍选择 Taro 作为京东鸿蒙原生应用的开发框架的原因,分析 Taro 在支持 Web 范式开发、快速迁移存量项目、渲染性能优化、高阶功能支持以及混合开发模式等方面的优势。

技术方案

京东在开发鸿蒙原生应用的过程中,需要考虑如何在有限的时间内高效完成项目,同时兼顾应用的性能与用户体验。为了达成这一目标,选择合适的技术方案至关重要。

在技术选型方面,开发一个鸿蒙原生应用,一般会有两种选择:

  • 使用原生 ArkTS 进行鸿蒙开发
  • 使用跨端框架进行鸿蒙开发

使用原生 ArkTS 进行鸿蒙开发,面临着开发周期冗长、维护多端多套应用代码成本高昂的挑战。在交付时间紧、任务重的情况下,京东果断选择跨端框架来开发鸿蒙原生应用,以期在有限的时间内高效完成项目。

作为在业界具备代表性的开源跨端框架之一,Taro 是由京东凹凸实验室团队开发的一款开放式跨端跨框架解决方案,它支持开发者使用一套代码,实现在 H5、小程序以及鸿蒙等多个平台上的运行。

通过 Taro 提供的编译能力,开发者可以将整个 Taro 项目轻松地转换为一个独立的鸿蒙应用,无需额外的开发工作。

image.png

另外,Taro 也支持将项目里的部分页面以模块化的形式打包进原生的鸿蒙应用中,京东鸿蒙原生应用便是使用这种模式进行开发的。

京东鸿蒙原生应用的基础基建能力如路由、定位、权限等能力由京东零售 mpass 团队来提供,而原生页面的渲染以及与基建能力的桥接则由 Taro 来负责,业务方只需要将写好的 Taro 项目通过执行相应的命令,就可以将项目以模块的形式一键打包到鸿蒙应用中,最终在应用内渲染出对应的原生页面,整个过程简单高效。

技术特点

Taro 作为一款开放式跨端跨框架解决方案,在支持开发者一套代码多端运行的同时,也为开发鸿蒙原生应用提供了诸多便利。在权衡多方因素后,我们最终选择了 Taro 作为开发鸿蒙原生应用的技术方案,总的来说,使用 Taro 来开发鸿蒙原生应用会有下面几点优势:

支持开发者使用 Web 范式来开发鸿蒙原生应用

与鸿蒙原生开发相比,使用 Taro 进行开发的最大优点在于 Taro 支持开发者使用前端 Web 范式来开发鸿蒙原生应用,基于这一特点,我们对大部分 CSS 能力进行了适配

  • 支持常见的 CSS 样式和布局,支持 flex、伪类和伪元素
  • 支持常见的 CSS 定位,绝对定位、fixed 定位
  • 支持常见的 CSS 选择器和媒体查询
  • 支持常见的 CSS 单位,比如 vh、vw 以及计算属性 calc
  • 支持 CSS 变量以及安全区域等预定义变量

在编译流程上,我们采用了 Rust 编写的 LightningCSS,极大地提升了 CSS 文件的编译和解析速度

image.png

(图片来自 LightningCSS 官网)

在运行时上,我们参考了 WebKit 浏览器内核的处理流程,对于 CSS 规则的匹配和标脏进行了架构上的升级,大幅提升了 CSS 应用和更新的性能。

image.png

支持存量 Taro 项目的快速迁移

将现有业务适配到一个全新的端侧平台,无疑需要投入大量的人力物力。而 Taro 框架的主要优势,正是能够有效解决这种跨端场景下的项目迁移难题。通过 Taro,我们可以以极低的成本,在保证高度还原和高性能的前提下,快速地将现有的 Taro 项目迁移到鸿蒙系统上。

image.png

渲染性能比肩原生开发

在 Taro 转换鸿蒙原生页面的技术实现上,我们摒弃了之前使用 ArkTS 原生组件递归渲染节点树的方案将更多的运行时逻辑如组件、动效、测算和布局等逻辑下沉到了 C++ 层,极大地提升了页面的渲染性能。

另外,我们对于 Taro 项目中 CSS 样式的处理架构进行了一次整体的重构和升级,并引入布局引擎 Yoga,将页面的测量和布局放在 Taro 侧进行实现,基于这些优化,实现一套高效的渲染任务管线,使得 Taro 开发的鸿蒙页面在性能上足以和鸿蒙 ArkTS 原生页面比肩。

image.png

支持虚拟列表和节点复用等高阶功能

长列表渲染是应用开发普遍会遇到的场景,在商品列表、订单列表、消息列表等需要无限滚动的组件和页面中广泛存在,这些场景如果不进行特殊的处理,只是单纯对数据进行渲染和更新,在数据量非常大的情况下,可能会引发严重的性能问题,导致视图在一段时间内无法响应用户操作。

在这个背景下,Taro 在鸿蒙端提供了长列表类型组件(WaterFlow & List),并对长列表类型组件进行了优化,提供了懒加载、预加载和节点复用等功能,有效地解决大数据量下的性能问题,提高应用的流畅度和用户体验。

image.png

(图片来自 HarmonyOS 官网)

支持原生混合开发等多种开发模式

Taro 的组件和 API 是以小程序作为基准来进行设计的,因此在实际的鸿蒙应用开发过程中,会出现所需的组件和 API 在 Taro 中不存在的情况,因为针对这种情况,Taro 提供了原生混合开发的能力,支持将原生页面或者原生组件混合编译到 Taro 鸿蒙项目中,支持 Taro 组件和鸿蒙原生组件在页面上的混合使用

image.png

性能表现

京东鸿蒙原生应用性能数据

经过对 Taro 的屡次优化和打磨,使得京东鸿蒙原生应用取得了优秀的性能表现,最终首页的渲染耗时 1062ms,相比于之前的 ArkTS 版本,性能提升了 23.9%;商详的渲染耗时 560 ms,相比于之前的 ArkTS 版本,性能提升 74.2%

值得注意的是商详页性能提升显著,经过分析发现商详楼层众多,CSS 样式也复杂多样,因此在 ArkTS 版本中,在 CSS 的解析和属性应用阶段占用了过多的时间,在 CAPI 版本进行了 CSSOM 模块的架构升级后,带来了明显的性能提升。

iShot_2024-09-03_22.57.29.png

基于 Taro 开发的页面,在华为性能工厂的专业测试下,大部分都以优异的成绩通过了性能验收,充分证明了 Taro 在鸿蒙端的高性能表现。

总结和未来展望

Taro 目前已经成为一个全业务域的跨端开发解决方案,实现 Web 类(如小程序、Hybrid)和原生类(iOS、Android、鸿蒙)的一体化开发,在高性能的鸿蒙适配方案的加持下,业务能快速拓展到新兴的鸿蒙系统中去,可以极大满足业务集约化开发的需求。

未来计划

后续,Taro 还会持续在性能上进行优化,以更好地适配鸿蒙系统:

  • 将开发者的 JS 业务代码和应用框架层的 JS 代码与主线程的 UI 渲染逻辑分离,另起一条 JavaScript 线程,执行这些 JS 代码,避免上层业务逻辑堵塞主线程运行,防止页面出现卡顿、丢帧的现象。

image.png

  • 实现视图节点拍平,将不影响布局的视图节点进行整合,减少实际绘制上屏的页面组件节点数量,提升页面的渲染性能。

image.png

(图片来自 React Native 官网)

  • 实现原生性能级别的动态更新能力,支持开发者在不重新编译和发布应用的情况下,动态更新应用中的页面和功能。

总结

京东鸿蒙原生应用是 Taro 打响在鸿蒙端侧适配的第一枪,证明了 Taro 方案适配鸿蒙原生应用的可行性。这标志着 Taro 在多端统一开发上的新突破,意味着 Taro 将为更多的企业和开发者提供优秀的跨端解决方案,使开发者能够以更高的效率开发出适配鸿蒙系统的高性能应用。

京东鸿蒙原生应用体验视频