在音视频处理、实时滤镜和端侧大模型推理等计算密集场景中,ArkTS 适合做 UI 与业务粘合,但面对几十 MB 像素矩阵运算或高频浮点矩阵乘法时,单线程执行和 GC 停顿容易造成掉帧或音频卡顿。HarmonyOS 7.0(API 26)之前,跨语言 FFI 数据传递是常见技术债:4K 图片展开约 30MB,通过 Node-API 从 C++ 内存逐字节拷贝到 ArkTS 对象空间,单次拷贝要十几毫秒,而 60fps 每帧只有 16ms 预算,直接吃掉一整帧;频繁大块分配释放还会引发 ArkCompiler GC 抖动。
HarmonyOS 7.0 的 NDK 更新重点有两处:Node-API 正式支持 ArrayBuffer 跨语言零拷贝,并进一步完善 C++ 层多线程并行计算与 ArkTS Worker 协同调度底座。C++ 线程处理完的内存块,可通过指针移交或内存共享直接挂载到 ArkTS 上下文,省去序列化与内存拷贝。原文给出的对比是:深拷贝方案叠加高斯模糊和边缘检测时约 24fps,且偶发 GC 掉帧;C++ 多线程加 ArrayBuffer 零拷贝方案稳定 60fps,内存占用峰值下降一半。工程上,耗时的 C++ 接口应放入 ImageWorker.ets 执行,避免 FFI 同步调用阻塞 UI 主线程。
一、先理解 FFI 边界为什么贵
ArkTS 底层由 ArkCompiler 驱动,采用 Actor 并发模型。每个线程(主线程、Worker)拥有独立内存堆和执行上下文,无共享内存简化了并发,但跨线程、跨语言通信昂贵。标准 NAPI 从 C++ 返回普通 JS Object 或 Array 时,底层通常经历状态切换、在 ArkCompiler 年轻代或大对象空间分配内存、把 C++ 栈上或堆上数据逐字节 memcpy、再生成 JS 句柄。数据到 MB 级时,既耗 CPU,又会快速填满年轻代,触发 Minor GC 和 Stop-The-World,界面表现为掉帧。
零拷贝的关键是“外部内存接管”。C++ 用 malloc 或 new 在 Native Heap 分配内存,这块内存不受 ArkCompiler GC 管辖;再调用 NAPI 接口,用轻量级 JS ArrayBuffer 外壳包装裸指针。传给 ArkTS 的只是带指针和长度的“壳”,底层大块数据留在原地。ArkTS 可通过 TypedArray(如 Uint8Array)直接操作 Native 内存。代价是生命周期管理:GC 不知道 Native 内存何时释放,因此需要注册 C++ 回调,当 ArrayBuffer 被回收时,由虚拟机回调事先绑定的 C++ 析构函数,在其中执行 free 或 delete。
二、零拷贝流转链路
完整链路是:UI 触发,Worker 调度,C++ 线程池计算,零拷贝返回,再渲染。两次关键零开销操作:C++ 到 Worker,使用 napi_create_external_arraybuffer 把 Native 指针包装成 ArrayBuffer;Worker 到 UI 主线程,通过 postMessage 第二个参数指定 Transferable 对象,把 ArrayBuffer 所有权转移,实现跨 Actor 堆栈零拷贝。
三、关键配置与 C++ 多线程处理
CMakeLists 中需要引入 NAPI、日志和多线程库:- cmake_minimum_required(VERSION 3.4.1)
- project(NdkZeroCopyDemo)
- add_library(render_plugin SHARED render_plugin.cpp ImageProcessor.cpp)
- target_link_libraries(render_plugin PUBLIC libace_napi.z.so libhilog_ndk.z.so pthread)
复制代码
C++ 层可按行切分图像,用 std::vector<std::thread> 并发处理,再用 malloc 分配结果内存。核心代码形态如下:- uint8_t* ImageProcessor::ProcessHeavyImage(int width, int height, size_t& outSize) {
- outSize = width * height * 4;
- uint8_t* imageBuffer = (uint8_t*)malloc(outSize);
- if (!imageBuffer) return nullptr;
- std::memset(imageBuffer, 100, outSize);
- unsigned int hardware_concurrency = std::thread::hardware_concurrency();
- int numThreads = hardware_concurrency > 0 ? hardware_concurrency : 4;
- std::vector<std::thread> workers;
- int rowsPerThread = height / numThreads;
- for (int i = 0; i < numThreads; ++i) {
- int startRow = i * rowsPerThread;
- int endRow = (i == numThreads - 1) ? height : (i + 1) * rowsPerThread;
- workers.emplace_back(std::thread(processImageRegion, imageBuffer, startRow, endRow, width));
- }
- for (auto& t : workers) {
- if (t.joinable()) t.join();
- }
- return imageBuffer;
- }
复制代码
四、NAPI 零拷贝封装要点
异步任务可避免阻塞 ArkTS 线程。执行阶段在 NAPI 工作线程调用 C++ 多线程计算,完成阶段回到触发任务的 JS 线程上下文,用 napi_create_external_arraybuffer 把结果裸指针包装成 ArrayBuffer。必须注册释放回调:- void BufferFreeCallback(napi_env env, void* data, void* hint) {
- if (data != nullptr) {
- free(data);
- }
- }
- void ExecuteAsyncWork(napi_env env, void* data) {
- AsyncContext* context = static_cast<AsyncContext*>(data);
- context->resultBuffer = ImageProcessor::ProcessHeavyImage(context->width, context->height, context->bufferSize);
- }
- void CompleteAsyncWork(napi_env env, napi_status status, void* data) {
- AsyncContext* context = static_cast<AsyncContext*>(data);
- napi_value resultObj;
- if (status == napi_ok && context->resultBuffer != nullptr) {
- napi_status createStatus = napi_create_external_arraybuffer(
- env, context->resultBuffer, context->bufferSize,
- BufferFreeCallback, nullptr, &resultObj);
- if (createStatus == napi_ok) {
- napi_resolve_deferred(env, context->deferred, resultObj);
- } else {
- free(context->resultBuffer);
- }
- }
- napi_delete_async_work(env, context->work);
- delete context;
- }
复制代码
ArkTS 侧在 Worker 中等待该接口,并用 Transferable 把 ArrayBuffer 转给 UI 线程:- import worker, { ThreadWorkerGlobalScope, MessageEvents } from '@ohos.worker';
- import renderPlugin from 'librender_plugin.so';
- const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
- workerPort.onmessage = async (e: MessageEvents) => {
- let req = e.data;
- if (req.type === 'START_PROCESS') {
- let arrayBuffer: ArrayBuffer = await renderPlugin.processImageAsync(req.width, req.height);
- workerPort.postMessage({ type: 'PROCESS_DONE', data: arrayBuffer }, [arrayBuffer]);
- }
- };
复制代码
主线程拿到 ArrayBuffer 后,可通过 image.createPixelMap(buffer, opts) 构造 PixelMap,再交给 Image 组件显示。示例中宽度 1080、高度 1920,像素格式 RGBA_8888,alphaType 为 PREMUL。
五、避坑手册
第一,不是所有数据都适合零拷贝。只传十几个浮点数时,直接使用标准 napi_create_double 创建数组即可,强行零拷贝反而增加注册析构回调等上下文负担。
第二,严禁提前释放。使用 napi_create_external_arraybuffer 后,内存生死逻辑上已绑定给虚拟机 GC。不能在 C++ 同步作用域里主动 free,否则会出现悬挂指针、Double Free 或非法内存访问;只能等 ArkCompiler 判定 ArrayBuffer 无引用后触发 BufferFreeCallback,再安全释放。
第三,Worker 是重量级对象。每次 new worker.ThreadWorker 都会拉起新的 Ark/V8 虚拟机实例,并分配数十兆初始堆栈。不要在点击事件或高频逻辑中动态新建 Worker,应在业务入口预热全局单例 Worker 池,之后复用 postMessage 通道。
HarmonyOS 7.0 的 NDK 零拷贝与 C++ 多线程协同,把原本在 FFI 边界和 Actor 堆栈之间来回搬运的数据留在 Native 内存中,只传递控制块和所有权,适合大块图像、音视频帧和端侧推理张量等场景。但收益越大,越要遵守内存生命周期契约,并按数据规模选择传递方案。 |