一、背景
在动态化-鸿蒙跨端方案文章中,讲述了动态化适配鸿蒙的方案实现,当在鸿蒙系统进行 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 组件一个过程实现能看到,代码量还是比较惊人的,其他场景的实现读者可以参考官方文档进行尝试。
评论