首页 > 其他分享 >鸿蒙跨端实践-ArkTS和CAPI的混合开发实现

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

时间:2024-09-02 10:54:42浏览次数:11  
标签:初始化 ArkTS NULL nullptr CAPI 组件 ArkUI 跨端

一、背景

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

1. UI层级过多

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

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


2. 通讯流程长

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

鸿蒙跨端实践-ArkTS和CAPI的混合开发实现_初始化_02


3. 列表渲染性能差

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

鸿蒙跨端实践-ArkTS和CAPI的混合开发实现_初始化_03


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的混合开发实现_数组_04


3.如何使用

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

鸿蒙跨端实践-ArkTS和CAPI的混合开发实现_字符串_05


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的混合开发实现_初始化_06


3.2、其他场景实现

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

标签:初始化,ArkTS,NULL,nullptr,CAPI,组件,ArkUI,跨端
From: https://blog.51cto.com/u_15714439/11895847

相关文章

  • HarmonyOS开发实战:ArkTS接口绑定
    场景介绍通过napi_wrap将ArkTS对象与Native的C++对象绑定,后续操作时再通过napi_unwrap将ArkTS对象绑定的C++对象取出,并对其进行操作。使用示例接口声明、编译配置以及模块注册接口声明//index.d.tsexportclassMyObject{constructor(arg:number);plusOne:(......
  • Capital许可分析的最佳实践
    在快速变化的软件许可环境中,企业如何确保合规性并优化成本?Capital许可分析为此提供了解决方案。通过深入探讨Capital许可分析的最佳实践,并结合实际案例,本文将为企业展示如何实现合规与成本的双赢。一、明确目标与策略成功的Capital许可分析始于明确的目标与策略。某大型制造企业......
  • Capital许可分析的最佳实践
    在快速变化的软件许可环境中,企业如何确保合规性并优化成本?Capital许可分析为此提供了解决方案。通过深入探讨Capital许可分析的最佳实践,并结合实际案例,本文将为企业展示如何实现合规与成本的双赢。一、明确目标与策略成功的Capital许可分析始于明确的目标与策略。某大型制造企业......
  • Capital许可分析
    在数字化时代,软件已成为企业日常运营的核心。而Capital许可分析,作为确保软件合规使用与成本优化的关键环节,正日益受到企业的重视。本文将深入探讨Capital许可分析的重要性,以及它如何助力企业在竞争激烈的市场中脱颖而出。一、合规性的守护者随着软件行业的快速发展,许可合规性问......
  • Capital许可分配策略
    在数字化浪潮席卷全球的今天,软件许可管理已成为企业运营不可或缺的一环。Capital许可分配策略,作为业界领先的解决方案,旨在帮助企业实现智能管理、优化资源,进而提升整体运营效率。一、什么是Capital许可分配策略?Capital许可分配策略是一种基于企业实际需求的软件许可分配方案。它......
  • Capital软件许可管理
    在当今的数字化时代,软件已经成为企业运营不可或缺的一部分。而Capital软件,作为业界领先的解决方案,为企业提供了强大的功能和卓越的性能。然而,要充分发挥Capital软件的潜力,合理的许可管理至关重要。一、什么是Capital软件许可管理?Capital软件许可管理是一种综合性的解决方案,旨在......
  • 动态化-鸿蒙跨端方案介绍
    一、背景......
  • ArkTS---http数据请求
    前言:    要使用http请求,需在module.json5文件中添加网络管理权限"module":{"requestPermissions":[{"name":"ohos.permission.INTERNET"}]}一、使用步骤    1、导入http模块        HTTP数据请求功能主要由http模块......
  • ArkTS---保存应用数据
    前言---Preferences    用户首选项Preferences,适用于对轻量级的Key-Value结构的数据进行存取和持久化操作。    Key-Value数据结构:一种键值型的数据结构,Key是不重复的关键字,Value是数据值。    非关系型数据库:区别于关系型数据库,不保证遵循ACID特性......
  • ArkUI ARKTS 鸿蒙开发 装饰器
    @State 定义一个变量,可用于本页面的双向绑定 @Prop 定义一个接收参数的变量,随着父组件变化而变化,子组件更改的值会被父组件覆盖(不会更改父组件的值) @Link 定义一个变量,不允许定义值,需要通过父组件传入,可以做到父子组件双向绑定 @Provide装饰器和@Consume装饰器 ......