鸿蒙5.0【HarmonyOS原生开发】ArkTS和CAPI的混合开发

一、背景

当在鸿蒙系统进行UI渲染的时候,我们使用了系统的组件进行递归渲染。在iOS和Android也是借助各自系统组件进行的渲染,但是在鸿蒙系统会存在以下4个严重问题:

1. UI层级过多

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

1

2. 通讯流程长

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

2

3. 列表渲染性能差

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

3

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

4

3.如何使用

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

5

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

6


最后呢,很多开发朋友不知道需要学习那些鸿蒙技术?鸿蒙开发岗位需要掌握那些核心技术点?为此鸿蒙的开发学习必须要系统性的进行。

而网上有关鸿蒙的开发资料非常的少,假如你想学好鸿蒙的应用开发与系统底层开发。你可以参考这份资料,少走很多弯路,节省没必要的麻烦。由两位前阿里高级研发工程师联合打造的《鸿蒙NEXT星河版OpenHarmony开发文档》里面内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、Harmony南向开发、鸿蒙项目实战等等)鸿蒙(Harmony NEXT)技术知识点

如果你是一名Android、Java、前端等等开发人员,想要转入鸿蒙方向发展。可以直接领取这份资料辅助你的学习。下面是鸿蒙开发的学习路线图。

​​​​1

高清完整版请点击《鸿蒙NEXT星河版开发学习文档》

针对鸿蒙成长路线打造的鸿蒙学习文档。话不多说,我们直接看详细资料鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,帮助大家在技术的道路上更进一步。

《鸿蒙 (OpenHarmony)开发学习视频》

《鸿蒙生态应用开发V2.0白皮书》

《鸿蒙 (OpenHarmony)开发基础到实战手册》

《鸿蒙开发基础》

《鸿蒙开发进阶》

《鸿蒙开发实战》
在这里插入图片描述

获取这份鸿蒙星河版学习资料,请点击→《鸿蒙NEXT星河版开发学习文档》

总结

鸿蒙—作为国家主力推送的国产操作系统。部分的高校已经取消了安卓课程,从而开设鸿蒙课程;企业纷纷跟进启动了鸿蒙研发。

并且鸿蒙是完全具备无与伦比的机遇和潜力的;预计到年底将有 5,000 款的应用完成原生鸿蒙开发,未来将会支持 50 万款的应用。那么这么多的应用需要开发,也就意味着需要有更多的鸿蒙人才。鸿蒙开发工程师也将会迎来爆发式的增长,学习鸿蒙势在必行!

  • 9
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值