鸿蒙跨端实践-ArkTS和CAPI的混合开发实现
一、背景
在动态化-鸿蒙跨端方案文章中,讲述了动态化适配鸿蒙的方案实现,当在鸿蒙系统进行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;