当前位置: 首页 > news >正文

鸿蒙JSVM-API深度解析:C++与JS跨语言交互实战指南

1. 项目概述与核心价值

最近在捣鼓鸿蒙5.0的Next版本,发现一个挺有意思的东西:JSVM-API。简单来说,它就像是在鸿蒙的JS应用和底层的C/C++代码之间,搭了一座桥。这玩意儿对于那些想在鸿蒙上做点高性能、或者需要复用大量现有C/C++库的开发者来说,简直是福音。你想想,JS写UI和业务逻辑很方便,但碰到音视频编解码、复杂图像处理、或者需要直接操作硬件的场景,性能可能就有点捉襟见肘了。这时候,如果能调用用C/C++写的、经过高度优化的原生模块,那体验和效率的提升就不是一点半点了。JSVM-API就是干这个的,它让你能在JS环境里,相对安全、高效地调用C/C++函数,还能在两种语言之间传递复杂的数据。这不仅仅是“能调用”,更关键的是它提供了一套相对规范的交互机制,包括内存管理、类型转换、异常处理等,让这种跨语言调用不再是“黑魔法”,而是有章可循的工程实践。

2. JSVM-API核心概念与交互模型解析

2.1 JSVM-API是什么?不是什么?

首先得明确,JSVM-API不是一个新的编程语言,也不是一个独立的运行时。它是鸿蒙ArkTS/JS运行时(特别是针对Next版本)提供的一组原生(Native)API接口。这套API的核心目标,是建立JavaScript(或TypeScript,最终编译为ArkTS Bytecode)与C/C++原生代码之间的互操作通道。你可以把它理解为一个精心设计的“外交协议”和“翻译官”。

它“是”什么:

  1. 一套C头文件(Header Files):主要包含jsvm_api.h等,定义了所有可供C/C++代码使用的数据结构、函数原型和宏。
  2. 一个运行时桥梁:它在JS引擎(如ArkTS引擎)和你的原生模块之间管理调用栈、内存和生命周期。
  3. 类型转换系统:自动或在开发者指导下,在JS的number,string,object,ArrayBuffer等类型与C/C++的int,double,char*,void*等类型之间进行转换。
  4. 错误处理机制:提供将C/C++侧的异常或错误,以JSError对象的形式抛回JS环境的能力。

它“不是”什么:

  1. 不是FFI(Foreign Function Interface)的简单封装:虽然目标类似,但JSVM-API深度集成在鸿蒙的ArkTS运行时中,考虑了鸿蒙应用模型、线程模型和安全沙箱,不是通用的跨平台FFI。
  2. 不是自动绑定生成器:它不会像一些工具那样,自动扫描你的C++头文件然后生成JS绑定代码。你需要手动编写适配层代码(通常是一个C接口),明确地描述每个要暴露的函数如何转换参数和返回值。
  3. 不是直接的内存共享:JS和C/C++有各自独立的内存空间。JSVM-API在传递数据时,多数情况下涉及拷贝(如字符串)。对于大数据(如图像帧),它提供了ArrayBufferNativeBindingObject等机制来减少拷贝开销,但内存管理责任需要开发者清晰界定。

2.2 交互模型:JS如何“看见”C/C++

理解交互模型是正确使用JSVM-API的关键。整个过程可以类比为一次远程过程调用(RPC),只不过“远程”是在同一个进程内的不同语言环境。

核心流程如下:

  1. 原生模块编译:你的C/C++代码(我们称之为“原生模块”)被编译成一个动态链接库(.so文件)。这个库需要链接鸿蒙的NDK中提供的JSVM相关库。
  2. 模块注册:在原生模块的入口处,你需要实现一个特定的函数(例如Init函数)。在这个函数里,你使用JSVM-API提供的函数,向JS运行时“注册”你这个模块,并声明你要暴露哪些函数、属性或类给JS。
  3. JS侧加载:在你的ArkTS/JS代码中,使用鸿蒙提供的importload机制(具体语法可能因版本而异,例如import nativeModule from 'libmyNative.so')来加载这个.so文件。运行时系统会找到并执行你模块中的Init函数。
  4. 函数调用:注册成功后,你在JS中就可以像调用普通JS函数一样,调用你暴露的原生函数了。例如:
    // ArkTS/JS 侧 import nativeModule from 'libmyNative.so'; let result = nativeModule.add(1, 2); // 调用C++函数 console.log(`Result from C++: ${result}`);
  5. 参数与返回值的“翻译”:当你调用nativeModule.add(1, 2)时:
    • JS运行时将参数12封装成JSVM内部表示。
    • 通过JSVM-API桥梁,调用到你C++模块中对应的add函数。
    • 在你的C++add函数实现里,你需要使用JSVM-API(如JsValueToNumber)将传入的JSVM参数句柄(JsiValueRef)转换为C++的int
    • 执行加法运算。
    • 使用JSVM-API(如NumberToJsValue)将C++的int结果转换回JSVM值句柄,并返回。
    • JS运行时接收这个句柄,将其转换回JS的number类型,赋值给result

线程模型注意事项:默认情况下,JS调用原生函数发生在JS线程(通常是UI线程)。如果你的C/C++函数执行耗时操作(如大量计算、文件IO、网络请求),会阻塞JS线程,导致界面卡顿。因此,对于耗时操作,最佳实践是:

  • 在C/C++侧创建独立的工作线程(pthread)来执行任务。
  • 使用JSVM-API提供的异步回调机制(如JsiCreateAsyncWork),在工作线程完成后,将结果回调到JS线程,并触发JS侧的Promise resolve或callback。

注意:跨线程传递JSVM值句柄(JsiValueRef)是危险的,因为它们与创建它们的线程上下文绑定。必须使用JsiCreateReference创建持久化引用,并在跨线程传递后,在正确的线程上下文中使用JsiAcquireValueJsiReleaseValue来安全地访问。这是内存泄漏和崩溃的高发区。

3. 核心API详解与内存管理要诀

3.1 关键数据结构与API分类

JSVM-API围绕几个核心数据结构展开:

  1. JsiRuntimeHandle:代表一个JS运行时环境。你的原生模块通常从回调函数中获取到当前调用的运行时句柄。所有后续的API调用大多需要这个句柄作为第一个参数,因为它指明了操作发生在哪个JS引擎实例中。
  2. JsiValueRef:这是最重要的类型,它是一个指向JS值的“不透明句柄”(opaque handle)。无论是JS传来的参数,还是你要返回给JS的值,都以JsiValueRef的形式在C/C++侧存在。你不能直接操作它内部的数据,必须通过JSVM-API函数来查询其类型、提取其值或创建新的值。
  3. JsiPropertyDescriptor:用于描述你要暴露给JS的对象属性或函数。你需要填充这个结构体,指定属性名、getter/setter、函数指针、属性特性(可写、可枚举、可配置)等。

API大致可以分为以下几类:

  • 运行时与上下文管理:如JsiGetCurrentRuntime
  • 值类型检查与转换
    • JsiValueIsNumber,JsiValueIsString,JsiValueIsObject,JsiValueIsArray等用于类型判断。
    • JsiValueToNumber,JsiValueToCString,JsiNumberToValue,JsiCreateStringUtf8等用于双向转换。
  • 对象与属性操作
    • JsiCreateObject,JsiCreateArray
    • JsiSetProperty,JsiGetProperty,JsiHasProperty
    • JsiDefineProperty(用于定义更复杂的属性,结合JsiPropertyDescriptor)
  • 函数相关
    • JsiCreateFunction:将C函数指针包装成JS可调用的函数。
    • JsiCallFunction:在C侧调用JS函数。
  • 错误处理JsiCreateError,JsiThrowError
  • 异步工作JsiCreateAsyncWork,JsiQueueAsyncWork
  • 内存与引用管理(重中之重)JsiCreateReference,JsiAcquireValue,JsiReleaseValue

3.2 内存管理:从“自动”到“手动”的思维转变

对于习惯JS自动垃圾回收(GC)的开发者来说,使用JSVM-API最大的挑战之一就是内存管理。在C/C++侧,你需要手动管理JS值的生命周期,否则极易导致内存泄漏或悬空指针。

核心原则:谁创建,谁(或约定方)释放;跨线程,需持久化。

1. 局部值的生命周期:在原生函数被调用时,传入的参数JsiValueRef和函数返回的JsiValueRef,其生命周期通常由JSVM运行时管理。你不需要也不应该去释放它们。例如:

// C++ 侧函数实现 JsiValueRef MyAdd(JsiRuntimeHandle runtime, JsiValueRef thisVal, int argc, JsiValueRef argv[]) { if (argc < 2 || !JsiValueIsNumber(argv[0]) || !JsiValueIsNumber(argv[1])) { // 创建错误并抛出 JsiValueRef error = JsiCreateError(runtime, "Invalid arguments"); JsiThrowError(runtime, error); // 注意:这里创建的error,如果Throw成功,运行时可能会接管其生命周期。 // 但最安全的做法是,如果Throw后函数立即返回,通常不需要手动释放。 return JsiCreateUndefined(runtime); } double a = JsiValueToNumber(runtime, argv[0]); // argv[0], argv[1] 不需要释放 double b = JsiValueToNumber(runtime, argv[1]); double sum = a + b; JsiValueRef result = JsiNumberToValue(runtime, sum); // 创建的result将由运行时在JS侧接收后管理 return result; // 返回给运行时,无需释放 }

2. 持久化引用(Persistent References):如果你需要在原生函数调用结束后,仍然保存某个JsiValueRef(例如,保存一个JS回调函数以备后续异步调用),你就必须创建“持久化引用”。

// 假设argv[0]是一个JS函数,我们想保存它 JsiValueRef jsCallback = argv[0]; // 错误!直接保存句柄,当本次JS调用结束,jsCallback可能被GC回收,变成悬空指针。 // someGlobalVariable = jsCallback; // 正确!创建持久化引用。 JsiRef ref = JsiCreateReference(runtime, jsCallback, 1); // 第三个参数1表示强引用 // 将ref保存到你的模块上下文或全局结构体中 myModuleContext->callbackRef = ref;

当你后续需要用到这个JS函数时:

JsiValueRef callbackValue = JsiAcquireValue(runtime, myModuleContext->callbackRef); if (callbackValue) { // 使用callbackValue调用JS函数 JsiValueRef args[] = { JsiCreateStringUtf8(runtime, "Async work done!") }; JsiValueRef result; JsiCallFunction(runtime, callbackValue, nullptr, args, 1, &result); // 调用完成后,释放Acquire得到的值 JsiReleaseValue(runtime, callbackValue); }

非常重要JsiAcquireValueJsiReleaseValue必须成对出现。Acquire告诉运行时“我现在要用了,别GC它”。Release告诉运行时“我用完了”。即使你持有JsiRef,在每次使用前也需要Acquire,使用后Release

3. 字符串内存管理:使用JsiValueToCStringJsiCreateStringUtf8时,需要注意返回的char*指针的生命周期。

  • JsiValueToCString返回的指针指向的内存可能是临时的。如果你需要长时间持有这个字符串,必须立即将其拷贝到自己的缓冲区(如std::stringmalloc的内存)。
  • JsiCreateStringUtf8创建的字符串,其内存在对应的JsiValueRef被GC回收时也会被管理。你不需要直接释放它。

4. 异步工作与内存安全:在异步回调中,你经常需要将数据从工作线程传递回JS线程。传递JsiValueRef是危险的。标准做法是:

  • 在工作线程启动前,使用JsiCreateReference为需要的JS值创建持久化引用。
  • JsiRef(它是一个整数或指针,不依赖运行时上下文)或你自定义的、包含必要原始数据(如int,double, 拷贝的char*)的结构体指针,传递给工作线程。
  • 在工作线程完成后的完成回调(这个回调在JS线程执行)中,使用JsiAcquireValue获取JS值,并进行后续操作,最后记得JsiReleaseValueJsiDeleteReference

实操心得:内存泄漏排查:一个常见的检查清单是:1) 每个JsiCreateReference是否都有配对的JsiDeleteReference?2) 每个JsiAcquireValue是否都有配对的JsiReleaseValue?3) 从JsiValueToCString得到的字符串,如果需长期使用,是否及时拷贝?可以使用简单的引用计数日志或鸿蒙提供的性能分析工具来辅助定位。

4. 完整实战:从C++模块到JS调用的全流程

让我们通过一个具体的例子,将一个C++的图像处理函数(模拟灰度化)暴露给JS。假设我们的C++库名为libimageProcessor.so

4.1 C++侧原生模块开发

第一步:定义头文件 (image_processor.h)

#ifndef IMAGE_PROCESSOR_H #define IMAGE_PROCESSOR_H #include <cstdint> #ifdef __cplusplus extern "C" { #endif // 暴露给JS的C接口函数声明 void* createProcessor(); void destroyProcessor(void* handle); bool processImage(void* handle, const uint8_t* inputData, int width, int height, uint8_t* outputData); #ifdef __cplusplus } #endif #endif

这里使用extern "C"确保函数名在C++编译后不被修饰(name mangling),以便JSVM能够正确找到。

第二步:实现C++核心类 (image_processor.cpp)

#include "image_processor.h" #include <vector> #include <algorithm> // 仅为示例,实际可能使用OpenCV等库 class ImageProcessorImpl { public: bool process(const uint8_t* input, int w, int h, uint8_t* output) { // 简单的灰度化模拟: (R + G + B) / 3 // 假设输入输出都是 RGBA 格式,每像素4字节 int totalPixels = w * h * 4; for (int i = 0; i < totalPixels; i += 4) { uint8_t r = input[i]; uint8_t g = input[i + 1]; uint8_t b = input[i + 2]; uint8_t gray = static_cast<uint8_t>((r + g + b) / 3); output[i] = gray; output[i + 1] = gray; output[i + 2] = gray; output[i + 3] = input[i + 3]; // Alpha通道保持不变 } return true; } }; // C接口实现 extern "C" { void* createProcessor() { return new ImageProcessorImpl(); } void destroyProcessor(void* handle) { if (handle) { delete static_cast<ImageProcessorImpl*>(handle); } } bool processImage(void* handle, const uint8_t* inputData, int width, int height, uint8_t* outputData) { if (!handle || !inputData || !outputData || width <=0 || height <=0) { return false; } ImageProcessorImpl* processor = static_cast<ImageProcessorImpl*>(handle); return processor->process(inputData, width, height, outputData); } }

第三步:编写JSVM适配层 (jsvm_adapter.cpp)这是最关键的一步,负责“翻译”。

#include "image_processor.h" #include "jsvm_api.h" #include <cstring> // 全局模块实例指针 static void* g_processorHandle = nullptr; // 1. 暴露给JS的函数:初始化处理器 static JsiValueRef NativeInitProcessor(JsiRuntimeHandle runtime, JsiValueRef thisVal, int argc, JsiValueRef argv[]) { if (g_processorHandle) { destroyProcessor(g_processorHandle); } g_processorHandle = createProcessor(); if (g_processorHandle) { return JsiCreateBoolean(runtime, true); } else { return JsiCreateBoolean(runtime, false); } } // 2. 暴露给JS的函数:处理图像 static JsiValueRef NativeProcessImage(JsiRuntimeHandle runtime, JsiValueRef thisVal, int argc, JsiValueRef argv[]) { // 参数检查:期望第一个参数是ArrayBuffer(输入图像数据),第二、三个是宽高(number) if (argc < 3 || !JsiValueIsArrayBuffer(runtime, argv[0]) || !JsiValueIsNumber(runtime, argv[1]) || !JsiValueIsNumber(runtime, argv[2])) { JsiThrowError(runtime, JsiCreateStringUtf8(runtime, "Invalid arguments. Expected (ArrayBuffer, width, height)")); return JsiCreateUndefined(runtime); } if (!g_processorHandle) { JsiThrowError(runtime, JsiCreateStringUtf8(runtime, "Processor not initialized. Call initProcessor first.")); return JsiCreateUndefined(runtime); } // 提取JS ArrayBuffer的信息 uint8_t* inputData = nullptr; size_t inputLength = 0; JsiGetArrayBufferInfo(runtime, argv[0], (void**)&inputData, &inputLength); int width = static_cast<int>(JsiValueToNumber(runtime, argv[1])); int height = static_cast<int>(JsiValueToNumber(runtime, argv[2])); // 验证数据长度是否匹配 (RGBA: width * height * 4) size_t expectedLength = width * height * 4; if (inputLength != expectedLength) { JsiThrowError(runtime, JsiCreateStringUtf8(runtime, "ArrayBuffer length does not match width*height*4")); return JsiCreateUndefined(runtime); } // 创建输出ArrayBuffer JsiValueRef outputArrayBuffer = JsiCreateArrayBuffer(runtime, expectedLength); uint8_t* outputData = nullptr; size_t outputLength = 0; JsiGetArrayBufferInfo(runtime, outputArrayBuffer, (void**)&outputData, &outputLength); // 调用C++核心函数 bool success = processImage(g_processorHandle, inputData, width, height, outputData); if (success) { return outputArrayBuffer; // 返回新的ArrayBuffer给JS } else { // 如果失败,确保不要返回无效的ArrayBuffer,可以返回undefined或null // 注意:outputArrayBuffer会被自动GC,因为我们已经不再持有它的引用(除了返回它)。 // 如果失败,我们不返回它,它就会被回收。 JsiThrowError(runtime, JsiCreateStringUtf8(runtime, "Image processing failed")); return JsiCreateUndefined(runtime); } } // 3. 暴露给JS的函数:释放处理器 static JsiValueRef NativeReleaseProcessor(JsiRuntimeHandle runtime, JsiValueRef thisVal, int argc, JsiValueRef argv[]) { if (g_processorHandle) { destroyProcessor(g_processorHandle); g_processorHandle = nullptr; } return JsiCreateUndefined(runtime); } // 4. 模块初始化函数 - 这个函数名是约定的(如Init),在JS加载.so时被调用 extern "C" __attribute__((visibility("default"))) void Init(JsiRuntimeHandle runtime) { // 创建一个JS对象,作为我们模块的命名空间 JsiValueRef moduleObj = JsiCreateObject(runtime); // 定义要暴露的函数 JsiPropertyDescriptor funcDesc[3] = {{0}}; // initProcessor 函数 funcDesc[0].name = JsiCreateStringUtf8(runtime, "initProcessor"); funcDesc[0].method = NativeInitProcessor; funcDesc[0].attributes = JSI_PROPERTY_ATTR_DEFAULT; // processImage 函数 funcDesc[1].name = JsiCreateStringUtf8(runtime, "processImage"); funcDesc[1].method = NativeProcessImage; funcDesc[1].attributes = JSI_PROPERTY_ATTR_DEFAULT; // releaseProcessor 函数 funcDesc[2].name = JsiCreateStringUtf8(runtime, "releaseProcessor"); funcDesc[2].method = NativeReleaseProcessor; funcDesc[2].attributes = JSI_PROPERTY_ATTR_DEFAULT; // 将这些属性定义到模块对象上 JsiDefineProperties(runtime, moduleObj, funcDesc, 3); // 将模块对象导出到JS全局环境(具体导出方式可能因鸿蒙版本略有不同,常见的是设置到global的某个属性下) // 假设约定是将对象设置到 globalThis.__imageProcessorModule JsiValueRef global = JsiGetGlobalObject(runtime); JsiSetProperty(runtime, global, JsiCreateStringUtf8(runtime, "__imageProcessorModule"), moduleObj); }

第四步:编写编译脚本 (CMakeLists.txt)

cmake_minimum_required(VERSION 3.12) project(imageProcessor) set(CMAKE_CXX_STANDARD 11) # 假设鸿蒙NDK路径已设置,这里需要链接JSVM库 find_library(JSVM_LIB jsvm) # 库名可能为 libjsvm.so 或类似 add_library(imageProcessor SHARED image_processor.cpp jsvm_adapter.cpp ) target_include_directories(imageProcessor PRIVATE ${OHOS_NDK_INCLUDE_DIR} # 鸿蒙NDK头文件路径,包含jsvm_api.h ) target_link_libraries(imageProcessor PRIVATE ${JSVM_LIB})

4.2 JS/ArkTS侧调用

第一步:配置项目依赖在你的鸿蒙应用项目的build-profile.json或模块级build.gradle中,确保包含对Native模块的构建支持,并将编译好的libimageProcessor.so打包到应用的libs/{abi}目录下。

第二步:编写ArkTS调用代码 (Index.ets)

// 导入原生模块。鸿蒙Next的具体导入语法请参考官方文档,这里是一种示意。 // 可能需要通过 `@ohos.napi` 或特定的 `loadNativeModule` 接口。 // 假设我们通过全局对象获取在Init函数中导出的模块 const nativeModule = globalThis.__imageProcessorModule; @Entry @Component struct Index { @State message: string = 'Hello, JSVM-API'; @State processedImageData: ArrayBuffer | null = null; // 存储处理后的图像数据 build() { Row() { Column() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) .onClick(() => { this.testImageProcessing(); }) // 这里可以添加一个Canvas组件,用于显示processedImageData if (this.processedImageData) { Canvas(this.processedImageData) // 假设有这样一个自定义组件 } } .width('100%') } .height('100%') } async testImageProcessing() { try { // 1. 初始化处理器 let initSuccess = nativeModule.initProcessor(); if (!initSuccess) { console.error('Failed to init processor'); return; } // 2. 准备测试图像数据 (模拟一个 2x2 的RGBA图像) const width = 2; const height = 2; // 像素数据: [R,G,B,A, R,G,B,A, ...] const inputData = new Uint8Array([ 255, 0, 0, 255, // 红 0, 255, 0, 255, // 绿 0, 0, 255, 255, // 蓝 255, 255, 0, 255 // 黄 ]); // 3. 调用原生处理函数 const outputArrayBuffer: ArrayBuffer = nativeModule.processImage(inputData.buffer, width, height); // 4. 处理结果 const outputData = new Uint8Array(outputArrayBuffer); console.log('Processed pixel (0,0):', outputData[0], outputData[1], outputData[2], outputData[3]); // 应该是灰度值 this.processedImageData = outputArrayBuffer; // 触发UI更新 this.message = 'Processing Done!'; // 5. 释放资源 (可选,或在页面销毁时调用) // nativeModule.releaseProcessor(); } catch (error) { console.error('JSVM-API call failed:', error); this.message = 'Error occurred'; } } }

5. 常见问题、调试技巧与性能优化

5.1 编译与链接问题

  • 未定义引用(undefined reference):确保你的CMakeLists.txt正确链接了libjsvm.so(或类似名称的库)。鸿蒙NDK中该库的路径和名称需确认。
  • Init函数找不到:确保你的适配层cpp文件中,Init函数使用了正确的导出修饰符(如extern "C" __attribute__((visibility("default")))),并且函数签名与运行时期望的完全一致(返回void,参数为JsiRuntimeHandle)。函数名可能严格区分大小写。
  • ABI不匹配:确保你编译的.so文件的ABI(armeabi-v7a, arm64-v8a, x86_64)与你的鸿蒙应用目标设备匹配。在应用打包时,需要为每个ABI提供对应的.so文件。

5.2 运行时崩溃与调试

  • 段错误(Segmentation Fault)
    • 最常见原因:访问了无效的JsiValueRef或已释放的JsiRef。仔细检查JsiAcquireValue/JsiReleaseValueJsiCreateReference/JsiDeleteReference是否配对。
    • 线程问题:在非创建JsiValueRef的线程中使用了它。确保所有JSVM API调用都发生在正确的线程(通常是调用原生函数的线程或通过JsiCreateAsyncWork指定的完成回调线程)。
    • 类型转换错误:在调用JsiValueToNumber等函数前,没有用JsiValueIsNumber进行检查。
  • 内存泄漏
    • 使用JsiCreateReference创建的每个引用,最终都必须用JsiDeleteReference删除。
    • 确保JsiAcquireValueJsiReleaseValue成对出现,即使在发生错误提前返回的情况下。
    • 可以考虑在Debug版本中,维护一个简单的引用计数日志。
  • 调试工具
    • HiLog:在C/C++代码中广泛使用鸿蒙的HiLog打印日志,这是追踪执行流程和变量值最基本有效的方法。
    • DevEco Studio Debugger:对于Native代码,可以配置C/C++调试器进行断点调试。需要将带符号的.so文件与源码关联。
    • ArkTS/JS Console:JS侧的异常和console.log输出是定位问题起点的重要依据。

5.3 性能优化建议

  1. 减少跨语言调用次数:每次JS到C++的调用都有开销。避免在循环中频繁调用简单的原生函数。尽量设计“一次调用,处理批量数据”的接口。
  2. 善用ArrayBuffer处理大数据:如图像、音频、文件数据,永远使用ArrayBuffer(或TypedArray.buffer)进行传递。如上述示例所示,这能最大程度减少内存拷贝。在C++侧通过JsiGetArrayBufferInfo获取指针直接操作内存。
  3. 异步化耗时操作:任何可能阻塞超过16ms(一帧时间)的操作,都必须放在工作线程中执行,并通过异步回调通知JS。使用JsiCreateAsyncWork是官方推荐的方式。
  4. 避免在C++侧创建大量临时JS对象:例如,在循环中不断JsiCreateStringUtf8来拼接字符串,效率极低。应在C++侧用std::stringstd::stringstream处理好,最后一次性创建JS字符串返回。
  5. 缓存持久化引用:对于需要重复使用的JS函数或对象(如回调函数),在初始化时创建一次持久化引用,而不是每次调用都去查找或创建。

5.4 类型映射与复杂对象传递

对于更复杂的数据结构,如JS对象,你需要手动解析:

// C++侧解析一个JS对象 { width: 100, height: 200, format: 'RGBA' } JsiValueRef configObj = argv[0]; int width = 0, height = 0; const char* format = nullptr; JsiValueRef widthVal = JsiGetProperty(runtime, configObj, JsiCreateStringUtf8(runtime, "width")); if (JsiValueIsNumber(runtime, widthVal)) { width = (int)JsiValueToNumber(runtime, widthVal); } // JsiGetProperty返回的临时值通常不需要Release,除非你创建了它的Reference。 JsiValueRef heightVal = JsiGetProperty(runtime, configObj, JsiCreateStringUtf8(runtime, "height")); if (JsiValueIsNumber(runtime, heightVal)) { height = (int)JsiValueToNumber(runtime, heightVal); } JsiValueRef formatVal = JsiGetProperty(runtime, configObj, JsiCreateStringUtf8(runtime, "format")); if (JsiValueIsString(runtime, formatVal)) { format = JsiValueToCString(runtime, formatVal, nullptr); // 注意:format指向的字符串内存是临时的!如果需要保存,必须立即拷贝。 std::string formatStr(format); // 立即拷贝 // ... 使用 formatStr }

反之,从C++返回一个复杂对象给JS,需要一步步创建:

JsiValueRef createResultObject(JsiRuntimeHandle runtime, int width, int height, bool success) { JsiValueRef resultObj = JsiCreateObject(runtime); JsiSetProperty(runtime, resultObj, JsiCreateStringUtf8(runtime, "width"), JsiNumberToValue(runtime, width)); JsiSetProperty(runtime, resultObj, JsiCreateStringUtf8(runtime, "height"), JsiNumberToValue(runtime, height)); JsiSetProperty(runtime, resultObj, JsiCreateStringUtf8(runtime, "success"), JsiBooleanToValue(runtime, success)); return resultObj; }

跨语言交互的代码总是比单语言更复杂,但一旦掌握了JSVM-API这套“外交辞令”和“交通规则”,你就能在鸿蒙生态中,让JS的灵活与C/C++的高效完美协作,开发出能力更强、体验更佳的应用。

http://www.cnnetsun.cn/news/3417192.html

相关文章:

  • EasyAR Unity插件:零代码构建AR应用与进阶开发指南
  • C++并发编程:深入理解std::thread::id线程标识符的设计与应用
  • Lovart Skills:设计经验的可执行封装与三层加载机制
  • DY-SV17F串口模式实战:从指令解析到多曲目轮播系统设计
  • 3分钟搞定原神分辨率适配:BetterGI一键自动调整终极指南
  • Cursor不是插件,是AI原生开发工作流的重新定义
  • C++ std::invalid_argument异常:从原理到实战的全面解析与防御性编程指南
  • 第十六届全国大学生智能车竞赛线上总决赛参赛队伍全景解析
  • 手机号码定位查询终极指南:3分钟学会免费获取精准地理位置信息
  • XUnity.AutoTranslator完整指南:3步实现Unity游戏智能实时翻译
  • Windows远程桌面实现之十二:桌面屏幕通过ONVIF协议与NVR等监控录像设备对接,以及进一步增强直播功能
  • 从开环到闭环:信捷PLC与台达脉冲伺服接线调试全记录
  • XSS漏洞
  • USB硬件设计实战:从引脚定义到热拔插安全机制
  • Neo4j知识图谱实战:从零构建到增删改查全流程指南
  • 电子病历自动生成与结构化:AI模型赋能医疗信息管理新路径
  • FPGA电源设计挑战与模块化解决方案
  • HermesAgent企业实战:大模型快速部署与微信飞书企微钉钉全平台集成
  • VM深度学习
  • 现代C++高性能IPC库cpp-ipc:基于共享内存的无锁通信实战指南
  • Claude Opus 4.7与GPT-5.5-Pro实测对比:代码生成与长文本处理深度评测
  • Docker 安装 IBM DB2 数据库教程-元一软件
  • Nginx入门指南-元一软件
  • 跨越天际:从智能汽车到 eVTOL 的适航与系统级开发43——多旋翼/倾转旋翼全包线动力学建模、姿态解算与控制律 Control Law 切换陷阱
  • TVA-具身智能:如何跨越电子与原子鸿沟(17)
  • TVA赋能下的具身智能技术进阶之路(6)
  • 从零构建高效可扩展的C++行为树框架:原理、实现与优化
  • 炉石传说脚本终极指南:3步配置,24小时自动刷金币
  • Python:使用segno模块打造个性化艺术二维码
  • SpringBoot Actuator端点安全加固实战:从风险识别到配置修复