开发者社区 > 博文 > 鸿蒙跨端实践-ArkTS和CAPI的混合开发实现
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

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

  • hb****
  • 2024-08-30
  • IP归属:北京
  • 100浏览

    一、背景

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

    1. UI层级过多

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

    2. 通讯流程长

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

    3. 列表渲染性能差

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

    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接口,流程大大缩短,数据类型转换变少了,列表渲染过程也由接入方自主控制,并且可以做预渲染等优化方案,同时避免了系统的二次布局。

    3.如何使用

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

    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)的深度已经和其他两端能对齐了。

    3.2、其他场景实现

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

    三、未来规划

       因华为尚未提供完整的CAPI文档,且当前能力不能满足我们所有诉求,我们将持续和华为保持沟通和验证,计划Q4将CAPI接入动态化鸿蒙SDK,进一步提升渲染性能,提升C端用户体验。