鸿蒙跨端实践-ArkTS和CAPI的混合开发实现

京东云开发者
• 阅读 259

一、背景

动态化-鸿蒙跨端方案文章中,讲述了动态化适配鸿蒙的方案实现,当在鸿蒙系统进行UI渲染的时候,我们使用了系统的组件进行递归渲染。在iOS和Android也是借助各自系统组件进行的渲染,但是在鸿蒙系统会存在以下4个严重问题:

1. UI层级过多

以金融APP理财频道页中的一个乐高楼层中的“7天理财”文案为例,鸿蒙系统总计52层,iOS30层。层级过多会直接影响渲染性能,到达一定层级后会造成页面掉帧和卡顿。



鸿蒙跨端实践-ArkTS和CAPI的混合开发实现 

2. 通讯流程长

在实现鸿蒙跨端方案中,JS虚拟机(V8)运行JS代码,通过JSI打通C++,再通过华为NAPI从C++打通ArkTS,跨语言通讯成本高。

 鸿蒙跨端实践-ArkTS和CAPI的混合开发实现 

3. 列表渲染性能差

长列表渲染性能是iOS、Android、Harmony系统非常重要的指标,华为也一直在推出多种方案以提升列表渲染性能。但在业界所有三方框架渲染长列表复杂业务场景(例如社区频道页面)时,在ArkUI层因设计原理导致性能问题一直无法完美解决。

 鸿蒙跨端实践-ArkTS和CAPI的混合开发实现 

4、二次布局

在对接到鸿蒙系统组件后,因为设置了相关布局属性后,系统会进行二次布局。

二、新方案实践

1.问题剖析

UI层级过多:原因在于在鸿蒙系统使用系统组件进行递归渲染的时候,需要借助自定义组件进行实现,然而和iOS和Android端的命令式组件渲染不同,比如RomaDiv对应iOS就是直接翻译为UIView即可,在鸿蒙必须增加一个包裹的容器才是一个合法的自定义组件,比如Stack容器,这样每个组件的层级就多了一层。

@Componentexport 
struct RomaDiv {
    build(){
        Stack(){
            //借助wrapBuilder实现递归
            ForEach(this.childrenTags, (childrenTag) => {
                  RomaComponentFactory.builder()//RomaComponentFactory就是对应鸿蒙系统提供的WrappedBuilder
            })
        }
    }
}

通讯流程长:js代码运行在系统内置的V8虚拟机中,ArkTS代码运行在华为的方舟虚拟机中,再加上V8运行js的线程,C++解析js指令的线程以及ArkTS的主线程,跨线程开销耗时增加,以及各个语言间的数据类型转换,通讯成本必然会非常高。

列表渲染性能差:鸿蒙的响应式编程,底层类似于vue做了依赖收集,虽然长列表场景下华为提供了cacheCount机制以提升列表渲染性能,但当数据发生变化的时候,数据的递归分析以及不在屏幕的的节点属性设置直接导致了列表性能的大幅下降。

二次布局:动态化在鸿蒙系统的跨端已经集成了另外两端共同使用的Yoga布局库,其实在给华为系统组件设置属性和坐标之前已经做好了布局计算,但是华为系统并未感知和处理这个过程,所以会存在二次布局的问题。

2.新方案简介

针对以上问题,通过和华为沟通,鸿蒙系统提供了C语言的命令式接口。C组件接口是介于UI组件的Native实现和ArkTS对接层之间的一层C接口封装,它绕过了状态管理对组件变化、刷新的自动化管理,同时避免了JS引擎和C++之间类型转换和跨语言调用的开销,因此具有较好的性能。

通过C接口的对接,UI层级能直接和另外两端基本一致,通讯过程直接从JS到C++,C++可以直接调用C接口,流程大大缩短,数据类型转换变少了,列表渲染过程也由接入方自主控制,并且可以做预渲染等优化方案,同时避免了系统的二次布局。

 鸿蒙跨端实践-ArkTS和CAPI的混合开发实现

3.如何使用

在实际的动态化鸿蒙跨端中,会存在ArkTS组件和C组件嵌套的场景(对于一些对性能影响较小的组件允许使用ArkTS),下面我们实现一个比较复杂的嵌套Demo,以展示整个嵌套实现过程。包含了ArkTS组件插入C组件、ArkTS组件插入ArkTS组件、C组件插入C组件、C组件插入ArkTS组件等场景。

 鸿蒙跨端实践-ArkTS和CAPI的混合开发实现



3.1、ArkTS插入C组件示例

ArkTS组件插入C组件的主要过程分为三步:

1、NodeContent管理器创建

2、build函数中的ContentSlot占位组件

3、NodeContent节点创建(CAPI)

import entry from 'libentry.so'; 
import { NodeContent } from '@ohos.arkui.node'

@Entry
@Component
struct CMixArkTS{ 
     //1、NodeContent管理器创建
     private divNodeContent: NodeContent = new NodeContent();
 }

build(){
    //2、build函数中的ContentSlot占位组件
    ContentSlot(this.divNodeContent);
}

aboutToAppear(): void {
    //3、NodeContent节点创建(CAPI)
    entry.CreateNativeDivNode(this.divNodeContent);
}

CreateNativeDivNode在C++中的实现如下:

此处有个坑: ArkUI_NativeNodeAPI_1 *nodeAPI 如果按照官方文档代码创建会失败,正确的方法如下代码所示。因为使用到ArkUI_NativeNodeAPI_1的地方比较多,所以我把ArkUI_NativeNodeAPI_1封装到CAPIManager::getNodeAPI()方法中了。

这个过程的核心API为OH_ArkUI_NodeContent_AddNode(nodeContentHandle_, DivComponent); 第一个参数指向ArkTS侧传入的nodeContent,第二个参数就是使用CAPI创建的Div节点。

// 1、C组件-绿色边框
static napi_value CreateNativeDivNode(napi_env env, napi_callback_info info) {
    // napi相关处理空指针&数据越界等问题
    if ((env == nullptr) || (info == nullptr)) {
        return nullptr;
    }

    napi_value returnVal = nullptr;

    size_t argc = 1;
    napi_value args[1] = {nullptr};
    if (napi_get_cb_info(env, info, &argc, args, nullptr, nullptr) != napi_ok) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "napi_init", "CreateNativeNode napi_get_cb_info failed");
    }

    if (argc != 1) {
        return nullptr;
    }
    // 将nodeContentHandle_指向ArkTS侧传入的nodeContent
    // 在Native侧获取ArkTS侧Content指针。
    OH_ArkUI_GetNodeContentFromNapiValue(env, args[0], &nodeContentHandle_);

    // nodeAPI = reinterpret_cast<ArkUI_NativeNodeAPI_1 *>(OH_ArkUI_QueryModuleInterfaceByName(ARKUI_NATIVE_NODE,
    // "ArkUI_NativeNode_API_1")); 上面写法不行,必须如下写法......
    // ArkUI_NativeNodeAPI_1 声明 ArkUI 提供的原生节点 API 集合。 与原生节点相关的 API 必须在主线程中调用。
    // 包括创建节点、添加、删除节点,给节点设置各种属性样式等
    static ArkUI_NativeNodeAPI_1 *nodeAPI = nullptr;
    if (nodeAPI == nullptr) {
        nodeAPI = reinterpret_cast<ArkUI_NativeNodeAPI_1 *>(
            OH_ArkUI_QueryModuleInterfaceByName(ARKUI_NATIVE_NODE, "ArkUI_NativeNodeAPI_1"));
    }

    if (nodeAPI != nullptr) {
        if (nodeAPI->createNode != nullptr && nodeAPI->addChild != nullptr) {
            ArkUI_NodeHandle DivComponent;
            // 创建div节点
            DivComponent = CreateDivNodeHandle();
            // nodeContentHandle_指向ArkTS侧传入的nodeContent,nodeContent上div节点
            OH_ArkUI_NodeContent_AddNode(nodeContentHandle_, DivComponent);
        }
    }

    return returnVal;
}

static ArkUI_NodeHandle CreateDivNodeHandle() {
    ArkUI_NodeHandle greenDivNodeHandle;
    // 创建div的node
    greenDivNodeHandle = CreateDivNodeHandleWithParam(200, 0xFF00FF00);
    CAPIManager::GetInstance()->greenDivNodeHandle = greenDivNodeHandle;
    return greenDivNodeHandle;
}

static napi_value Init(napi_env env, napi_value exports){
    { "CreateNativeDivNode", nullptr, CreateNativeDivNode, nullptr, nullptr, nullptr, napi_default, nullptr},
}

真正的C组件创建:

static ArkUI_NodeHandle CreateDivNodeHandleWithParam(float height, uint32_t borderColor) {
 ArkUI_NodeHandle divNode = CAPIManager::getNodeAPI()->createNode(ArkUI_NodeType::ARKUI_NODE_FLEX);
 // margin
 ArkUI_NumberValue number = {.f32 = 5};
 ArkUI_AttributeItem marginValue = {
 .value = &number, // 初始化为NULL或者指向你的数字数组
 .size = 1, // 初始化为你的数字数组的大小
 .string = NULL, // 初始化为NULL或者指向你的字符串
 .object = NULL // 初始化为NULL或者指向你的对象
 };

 // borderWidth
 ArkUI_NumberValue number2 = {.f32 = 2};
 ArkUI_AttributeItem borderWValue = {
 .value = &number2, // 初始化为NULL或者指向你的数字数组
 .size = 1, // 初始化为你的数字数组的大小
 .string = NULL, // 初始化为NULL或者指向你的字符串
 .object = NULL // 初始化为NULL或者指向你的对象
 };

 // 背景色
 ArkUI_NumberValue number1 = {.u32 = borderColor};
 ArkUI_AttributeItem borderColorItem = {
 .value = &number1, // 初始化为NULL或者指向你的数字数组
 .size = 1, // 初始化为你的数字数组的大小
 .string = NULL, // 初始化为NULL或者指向你的字符串
 .object = NULL // 初始化为NULL或者指向你的对象
 };

 // 宽高
 ArkUI_NumberValue number3 = {.f32 = height};
 ArkUI_AttributeItem hValue = {
 .value = &number3, // 初始化为NULL或者指向你的数字数组
 .size = 1, // 初始化为你的数字数组的大小
 .string = NULL, // 初始化为NULL或者指向你的字符串
 .object = NULL // 初始化为NULL或者指向你的对象
 };
 ArkUI_NumberValue number5 = {.f32 = 0.9};
 ArkUI_AttributeItem wValue = {
 .value = &number5, // 初始化为NULL或者指向你的数字数组
 .size = 1, // 初始化为你的数字数组的大小
 .string = NULL, // 初始化为NULL或者指向你的字符串
 .object = NULL // 初始化为NULL或者指向你的对象
 };

 ArkUI_NumberValue number4 = {.i32 = ARKUI_ITEM_ALIGNMENT_CENTER};
 ArkUI_AttributeItem alignment = {
 .value = &number4, // 初始化为NULL或者指向你的数字数组
 .size = 1, // 初始化为你的数字数组的大小
 .string = NULL, // 初始化为NULL或者指向你的字符串
 .object = NULL // 初始化为NULL或者指向你的对象
 };
 // 属性设置

 CAPIManager::getNodeAPI()->setAttribute(divNode, NODE_MARGIN, &marginValue);
 CAPIManager::getNodeAPI()->setAttribute(divNode, NODE_BORDER_WIDTH, &borderWValue);
 CAPIManager::getNodeAPI()->setAttribute(divNode, NODE_BORDER_COLOR, &borderColorItem);
 CAPIManager::getNodeAPI()->setAttribute(divNode, NODE_WIDTH_PERCENT, &wValue);
 CAPIManager::getNodeAPI()->setAttribute(divNode, NODE_HEIGHT, &hValue);
 CAPIManager::getNodeAPI()->setAttribute(divNode, NODE_ALIGN_SELF, &alignment);

 return divNode;
}

通过以上过程可以发现,通过CAPI创建一个节点并渲染的过程还是比较复杂的,但只要抓住实现过程的核心步骤,剩下的就是按照文档开发就行了。

大家感受下iOS实现这个过程的模拟:

- (UIView *) CreateNativeDivNode{
    UIView* div = [UIView new]; 
    div.backGroundColor = [UIColor greenColor];
    div.frame = CGRectMake(0,0,width,height);
    return div;
}

虽然过程有点复杂,但是效果还是不错的,毕竟能解决文章开头提出的4个问题。比如最直观的UI层级,Text26(I am A ArkTS Node)的深度已经和其他两端能对齐了。

 鸿蒙跨端实践-ArkTS和CAPI的混合开发实现 

3.2、其他场景实现

从上面ArkTS组件插入C组件一个过程实现能看到,代码量还是比较惊人的,其他场景的实现读者可以参考官方文档进行尝试。

点赞
收藏
评论区
推荐文章
京东云开发者 京东云开发者
12个月前
使用 Taro 开发鸿蒙原生应用 —— 探秘适配鸿蒙 ArkTS 的工作原理
背景在上一篇文章中,我们已经了解到华为即将发布的鸿蒙操作系统,以及各个互联网厂商开展鸿蒙应用开发的消息。其中,Taro作为一个重要的前端开发框架,也积极适配鸿蒙的新一代语言框架——ArkTS。本文将深入探讨Taro适配鸿蒙ArkTS框架的工作原理,接下来我
爱学it学无止境 爱学it学无止境
5个月前
2024 鸿蒙零基础快速实战-仿抖音App开发( ArkTS版 )|完结
ArkTS:鸿蒙应用开发的高效利器ArkTS,作为HarmonyOS4.0优选的主力应用开发语言,正逐步成为鸿蒙生态系统中的核心开发工具。它基于TypeScript进行扩展,不仅继承了TypeScript的静态类型优势,还通过一系列创新设计,进一步提升了
京东云开发者 京东云开发者
3个月前
动态化-鸿蒙跨端方案介绍
一、背景👉华为在2023.9.25官方发布会上宣布,新的鸿蒙系统将不再兼容安卓应用,这意味着,包括京东金融APP在内的所有安卓应用,在新的鸿蒙系统上将无法运行,需要重新开发专门适用于新鸿蒙系统的专版APP。二、原生适配方案原生适配方案就是将京东金融APP
京东云开发者 京东云开发者
1个月前
Taro 鸿蒙技术内幕系列(一):如何将 React 代码跑在 ArkUI 上
作者:京东零售朱鸣辉基于Taro打造的京东鸿蒙APP已跟随鸿蒙Next系统公测,本系列文章将深入解析Taro如何实现使用React开发高性能鸿蒙应用的技术内幕背景随着鸿蒙操作系统的快速发展,开发者们期待将现有跨平台应用迁移到鸿蒙平台。Taro作为一个流行的
京东云开发者 京东云开发者
1个月前
Taro 鸿蒙技术内幕系列(二):如何让 W3C 标准的 CSS跑在鸿蒙上
作者:京东零售马银涛基于Taro打造的京东鸿蒙APP已跟随鸿蒙Next系统公测,本系列文章将深入解析Taro如何实现使用React开发高性能鸿蒙应用的技术内幕背景HarmonyOS采用自研的ArkUI框架作为原生UI开发方案,这套方案有完善的布局系统和样式
田楷 田楷
3星期前
鸿蒙Flutter实战:06-使用ArkTs开发Flutter鸿蒙插件
使用ArkTs开发Flutter鸿蒙平台插件本文讲述如何开发一个Flutter鸿蒙插件,如何实现Flutter与鸿蒙的混合开发,以及双端消息通信。Flutter侧,编写MethodChanneldartconstMethodChannelmethodCha
京东云开发者 京东云开发者
3星期前
Taro 鸿蒙技术内幕系列(三) - 多语言场景下的通用事件系统设计
作者:京东零售朱天健基于Taro打造的京东鸿蒙APP已跟随鸿蒙Next系统公测,本系列文章将深入解析Taro如何实现使用React开发高性能鸿蒙应用的技术内幕背景在鸿蒙生态系统中,虽然原生应用通常基于ArkTS实现,但在实际研发过程中发现,使用C可以显
京东云开发者 京东云开发者
2星期前
Taro 鸿蒙技术内幕系列(四):JDImage 自研鸿蒙图片库
作者:京东零售何骁基于Taro打造的京东鸿蒙APP已跟随鸿蒙Next系统公测,本系列文章将深入解析Taro如何实现使用React开发高性能鸿蒙应用的技术内幕背景2024年初,京东正式启动了鸿蒙APP的开发工作。由于电商APP大量依赖图片来展示商品信息,对图