《HarmonyOSNext 性能暴增秘籍:Node-API 多线程通信从阻塞到丝滑的 4 大方案实战》
##Harmony OS Next ##Ark Ts ##教育
本文适用于教育科普行业进行学习,有错误之处请指出我会修改。
🚀 引言:为啥要异步?搞懂线程才是王道!
兄弟姐妹们!做 Native 开发(尤其是 C/C++)的时候,有没有遇到过这种场景?🤔
👉 场景一: 算个超简单的数,主线程就想蹲那儿等结果立马用。这时候搞个同步开发,利索!🛸
👉 场景二: 碰到个计算怪兽🧟♂️(比如读超大文件、搞复杂图片处理),处理起来贼拉慢!这时候你还让主线程傻等?NoNoNo!主线程卡死了,App 界面动不了,用户立马想摔手机!💥
✅ 解决方案:上异步! 把这些耗时的脏活累活扔给 Native 侧的后台小线程去干!主线程该蹦迪蹦迪,该刷新 UI 刷新 UI,一点都不耽误!🕺💃
但是!问题来了❗️后台小线程干完活,结果怎么告诉主线程呢?主线程得用它来刷新界面呀(比如显示张刚加载好的帅照)!
别急!这就带大家撸一遍核心玩法:
本文就用一个超实用的图片加载案例,手把手教你咋玩转这四种模式!包会!😎
🗺️ 典型开发场景快速扫盲
先来个一分钟看懂几种玩法核心区别:
小提示💡: 同步也能带 Callback 哦!看开发者心情~ (主要看参数是否传了 Callback 函数)
下面深入讲讲我们的主角案例是怎么设计的!
🎮 案例登场:一个图片加载器搞定所有!
🧠 设计思路
目标:搞一个 UI,点不同按钮,用不同姿势(同步、callback、promise、线程安全)加载图片!还有个调皮按钮专门显示错误图片弹窗~ 😈
架构分成两大块:ArkTS 应用侧 + Native 侧。看图⬇️ (脑补效果图)
+-----------------------+ +-------------------------+| ArkTS应用侧 | | Native侧 || | | || [同步按钮] [Callback] | ---> | Native接口部分 || [Promise] [TSF] | | 🧐 核心逻辑处理! || | | || +-------------------+ | | || | 图片显示区 | | <---- | 生产者-消费者模型 || +-------------------+ | | (幕后大佬干重活!) |+-----------------------+ +-------------------------+
复制代码
🪶 ArkTS 应用侧:点啥按钮,干啥活!
import testNapi from 'libentry.so'; // 引入我们写好的Native模块import Constants from '...'; // 路径常量
@Entry@Componentstruct Index { @State imagePath: string = Constants.INIT_IMAGE_PATH; // 默认图片路径 imageName: string = ''; // 要加载的图片名
build() { Column() { // ... (布局代码) Column() { // 1️⃣ 同步调用按钮 Button('多线程同步调用 🐢') // 🐢表示有点慢,主线程等 .onClick(() => { this.imageName = 'sync_img'; // 设置图片名 // ⚠️ 调用Native同步接口!界面在结果返回前会卡一下! this.imagePath = Constants.IMG_ROOT + testNapi.getImagePathSync(this.imageName); })
// 2️⃣ Callback异步按钮 Button('Callback异步 📱') .onClick(() => { this.imageName = 'callback_img'; // 调用异步接口,传入一个callback函数等着被call testNapi.getImagePathAsyncCallBack(this.imageName, (result: string) => { this.imagePath = Constants.IMG_ROOT + result; // Native干完会call这里! }); })
// 3️⃣ Promise异步按钮 Button('Promise异步 🤝') .onClick(() => { this.imageName = 'promise_img'; // 拿到一个Promise对象!用.then等结果 testNapi.getImagePathAsyncPromise(this.imageName).then((result: string) => { this.imagePath = Constants.IMG_ROOT + result; }); })
// 4️⃣ TSF线程安全异步按钮 Button('TSF线程安全 🔐') .onClick(() => { this.imageName = 'tsf_img'; // 也是callback接收结果,但底层用线程安全函数通信 testNapi.getImagePathAsyncTSF(this.imageName, (result: string) => { this.imagePath = Constants.IMG_ROOT + result; }); })
// 5️⃣ 调皮鬼按钮:错误图片测试 🐒 Button('错误图片测试 ❌') .onClick(() => { // 传入错误图片名,触发弹窗! this.imageName = 'wrong_img'; testNapi.getImagePathSync(this.imageName); // 这里调用哪个接口都可能触发 }) } // ... (显示图片的组件) } }}
复制代码
🦾 Native 侧:两大硬核组件发力!
Native 接口部分: 接收 ArkTS 的命令,决定咋干活。
同步处理: 直接在当前线程(主线程!)上干活 -> 调生产者消费者模型找图 -> 返回结果给 ArkTS。
异步处理 (Callback/Promise/TSF): 创建异步任务(工作项或线程安全函数) -> 扔后台线程干 -> 干完想办法把结果传回 ArkTS 主线程刷新 UI。
生产者-消费者模型 (Producer-Consumer Model): 🧠 大脑级组件!负责最重的找图任务!
生产者线程🧵: 拿着图片名吭哧吭哧搜索🔍,找到路径放进一个缓冲队列。
消费者线程🧵: 从队列里取路径结果,然后通过特定方法把结果传回 ArkTS 主线程🚀。
为啥用这个模型?🤔 完美协调“生产”和“消费”速度!队列满了生产等,队列空了消费等。信号量、条件变量、互斥锁上!安全又高效!✨
🧱 生产者-消费者模型核心实现 (Show me the code!)
关键文件头 (ProducerConsumer.h):
// ProducerConsumer.h#ifndef MULTITHREADS_PRODUCERCONSUMER_H#define MULTITHREADS_PRODUCERCONSUMER_H
#include <string>#include <queue>#include <mutex>#include <condition_variable>
using namespace std;
class ProducerConsumerQueue {public: // 构造函数,可以设置队列最大容量 ProducerConsumerQueue(int queueSize = 1) : m_maxSize(queueSize) {} // 默认容量1(一次处理一张图)
// ⭐ 生产者:把元素(图片路径)放进队列 void PutElement(string element);
// ⭐ 消费者:从队列里取出元素(图片路径) string TakeElement();
private: bool isFull() { return (m_queue.size() == m_maxSize); } // 队列满了吗? bool isEmpty() { return m_queue.empty(); } // 队列空了吗?
private: queue<string> m_queue{}; // 核心缓冲队列 int m_maxSize{}; // 队列最大容量 mutex m_mutex{}; // 🛡️ 互斥锁,保护队列操作安全! condition_variable m_notEmpty{}; // 😴 条件变量:通知"队列不空了!消费者快来取!" condition_variable m_notFull{}; // 😴 条件变量:通知"队列不满啦!生产者快来放!"};#endif
复制代码
具体实现 (ProducerConsumer.cpp):
// ProducerConsumer.cpp#include "ProducerConsumer.h"
// 生产者:放元素进队列void ProducerConsumerQueue::PutElement(string element) { unique_lock<mutex> lock(m_mutex); // 🔐 先加锁!
while (isFull()) { // 满了?老实等着消费者吃掉点腾地方! m_notFull.wait(lock); // 😴 等"不满"的信号(锁会自动释放) } // 不满了!把元素放进去,然后喊一嗓子:"不空啦!快来取!" m_queue.push(element); m_notEmpty.notify_one(); // 📣 唤醒一个等着的消费者}
// 消费者:从队列里拿元素string ProducerConsumerQueue::TakeElement() { unique_lock<mutex> lock(m_mutex); // 🔐 先加锁!
while (isEmpty()) { // 空的?老实等着生产者放点东西进来! m_notEmpty.wait(lock); // 😴 等"不空"的信号(锁自动释放) } // 不空了!拿走队头元素,然后喊一嗓子:"不满啦!快来放!" string element = m_queue.front(); m_queue.pop(); m_notFull.notify_one(); // 📣 唤醒一个等着的生产者 return element;}
复制代码
⚙️ Native 侧后台运作 (MultiThreads.cpp)
全局变量 & 搜索逻辑
// MultiThreads.cpp// 静态全局的缓冲队列实例(一次处理一张图,容量=1)static ProducerConsumerQueue buffQueue(1);
// 假装我们有这些图片路径(实际开发可能从数据库、网络、文件系统读取)static vector<string> imagePathVec{"sync.png", "callback.png", "promise.png", "tsf.png"};
// 辅助函数:检查图片路径名和输入名是否匹配(忽略后缀)static bool CheckImagePath(const string &imageName, const string &imagePath) { size_t pos = imagePath.find('.'); if (pos == string::npos) return false; // 无效路径 string nameOnly = imagePath.substr(0, pos); return (imageName == nameOnly); // 只比较名字部分}
// 🕵️♀️ 核心搜索函数:按图片名找路径static string SearchImagePath(const string &imageName) { for (const string &imgPath : imagePathVec) { if (CheckImagePath(imageName, imgPath)) { return imgPath; // 找到啦! } } return string(""); // 没找到!😢}
复制代码
生产者线程函数:找图塞队列
static void ProductElement(void *data) { // 从上下文数据里拿到图片名,搜路径,放进队列 ContextData *ctx = static_cast<ContextData*>(data); buffQueue.PutElement(SearchImagePath(ctx->args));}
复制代码
消费者线程函数:取结果传回去
static void ConsumeElement(void *data) { ContextData *ctx = static_cast<ContextData*>(data); ctx->result = buffQueue.TakeElement(); // 从队列拿结果存起来(后续Native接口负责传回ArkTS)}* **线程安全(TSF)异步:**static void ConsumeElementTSF(void *data) { ContextData *ctx = static_cast<ContextData*>(data); ctx->result = buffQueue.TakeElement();
// 🔧 TSF关键步骤: // 1️⃣ 锁住线程安全函数(防止它在回调前被销毁) (void)napi_acquire_threadsafe_function(tsFun); // 2️⃣ 🔥 核心!把包含结果的数据和线程安全函数(tsFun)一起发给主线程EventLoop (void)napi_call_threadsafe_function(tsFun, data, napi_tsfn_blocking); // 阻塞模式确保发送成功 // 3️⃣ 释放TSF的引用 (void)napi_release_threadsafe_function(tsFun, napi_tsfn_release);}
复制代码
🔍 深入核心:四种模式怎么玩?
🐢 模式 1:同步调用 - 简单直接但会卡一下
📜 原理图脑补:
ArkTS主线程(点击按钮) ---> [Native同步接口] ---> (主线程等) ---> 创建生产者/消费者线程并等它们干完活(join()) ---> 拿到结果返回 ---> ArkTS主线程刷新UI
复制代码
👨💻 Native 接口开发步骤 (MultiThreads.cpp):
// ✨ 1. 导出接口给ArkTS用 (index.d.ts)export const getImagePathSync: (imageName: string) => string;
// 📦 2. 上下文数据结构 (保存参数和结果)struct ContextData { // ... (可能有异步相关的成员,同步不用,但结构保持统一) string args = ""; // ArkTS传来的图片名 string result = ""; // 计算结果(图片路径)};
// 🗑️ 3. 任务结束销毁上下文 (通用)static void DeleteContext(napi_env env, ContextData *ctx) { ... } // 略,见后面通用部分
// 🧩 4. Native同步接口实现static napi_value GetImagePathSync(napi_env env, napi_callback_info info) { // ... (解析参数:图片名imageName)
// 创建上下文数据 auto ctxData = new ContextData; ctxData->args = imageName;
// 💪 同步干活:创建并等待生产者和消费者线程join完成! thread producer(ProductElement, static_cast<void*>(ctxData)); producer.join(); // 👉 主线程在这等生产者干完!
thread consumer(ConsumeElement, static_cast<void*>(ctxData)); consumer.join(); // 👉 主线程在这等消费者干完!
// 🎉 拿到结果了!转换成ArkTS能用的类型(string)返回 napi_value resultValue; napi_create_string_utf8(env, ctxData->result.c_str(), ctxData->result.length(), &resultValue);
// 🧹 扫尾:删掉上下文数据 DeleteContext(env, ctxData);
return resultValue; // ⬅️ 直接把这个值扔回给ArkTS主线程!}
复制代码
🌟 总结同步特点:
📱 模式 2:Callback 异步 - 打完 Call 我!
📜 原理图脑补:
ArkTS主线程(点击按钮) --> [Native Callback接口] --> (立刻返回空值/null) --> ✅主线程继续爽滑操作UI ↓ (后台创建异步工作项入队) libuv线程池调度(work子线程) ---> execute回调干活(找图) ---> 主线程EventLoop ---> complete回调 ---> 执行你的Callback函数通知结果刷新UI
复制代码
👨💻 Native 接口开发步骤 (MultiThreads.cpp):
// ✨ 1. 导出接口给ArkTS用 (index.d.ts)export const getImagePathAsyncCallBack: (imageName: string, callBack: (result: string) => void) => void;
// 📦 2. 上下文数据结构 (重要!)struct ContextData { napi_async_work asyncWork = nullptr; // ✨异步工作项对象! napi_ref callbackRef = nullptr; // 🔗保存ArkTS传来的Callback函数的引用 string args = ""; string result = "";};
// 🗑️ 3. 销毁上下文函数 (通用)static void DeleteContext(napi_env env, ContextData *ctx) { if (ctx->callbackRef) napi_delete_reference(env, ctx->callbackRef); // 🧹删Callback引用 if (ctx->asyncWork) napi_delete_async_work(env, ctx->asyncWork); // 🧹删异步工作项 delete ctx; // 🧹删上下文内存}
// ⚙️ 4.1 execute回调 (libuv work子线程执行,不许碰napi!)static void ExecuteFunc([[maybe_unused]] napi_env env, void *data) { ContextData *ctx = static_cast<ContextData*>(data); // 干活:创建生产者消费者线程干活!⚠️必须在execute里join等它们干完! thread producer(ProductElement, data); producer.join(); thread consumer(ConsumeElement, data); consumer.join();}
// 📨 4.2 complete回调 (主线程EventLoop执行,能调用napi!)static void CompleteFuncCallBack(napi_env env, [[maybe_unused]] napi_status status, void *data) { ContextData *ctx = static_cast<ContextData*>(data); // 1️⃣ 从引用里拿出ArkTS传的Callback函数 napi_value jsCallback; napi_get_reference_value(env, ctx->callbackRef, &jsCallback);
// 2️⃣ 准备参数:计算结果(图片路径) napi_value callbackArg; napi_create_string_utf8(env, ctx->result.c_str(), ctx->result.length(), &callbackArg);
// 3️⃣ 调用ArkTS的Callback函数!把结果传回去! napi_value undefined; napi_get_undefined(env, &undefined); // Callback的this用undefined napi_value ignore; napi_call_function(env, undefined, jsCallback, 1, &callbackArg, &ignore); // 调用Callback!
// 4️⃣ 🧹 销毁上下文 DeleteContext(env, ctx);}
// 🧩 5. Native Callback接口实现static napi_value GetImagePathAsyncCallBack(napi_env env, napi_callback_info info) { // ... (解析参数:imageName 和 callback函数)
// 📦 创建并初始化上下文 auto ctxData = new ContextData; ctxData->args = imageName; napi_create_reference(env, callbackFunc, 1, &ctxData->callbackRef); // ⭐存Callback引用!
// 🪄 创建异步工作项 (核心!) napi_value asyncName; napi_create_string_utf8(env, "AsyncCallBackJob", NAPI_AUTO_LENGTH, &asyncName); napi_create_async_work( // ⭐⭐关键API! env, nullptr, asyncName, ExecuteFunc, // 后台执行函数 CompleteFuncCallBack, // 完成回调函数(主线程) static_cast<void*>(ctxData), &ctxData->asyncWork // 返回的工作项对象存到ctx里 );
// 🚀 把异步工作项扔进调度队列! napi_queue_async_work(env, ctxData->asyncWork);
return nullptr; // ⚠️ 注意!立刻返回null给ArkTS!}
复制代码
🌟 总结 Callback 异步特点:
ArkTS 主线程调用后立刻返回null,丝滑流畅!🥰
耗时活在 work 子线程(ExecuteFunc)做。
结果在主线程的回调函数(CompleteFuncCallBack)中用napi_call_function调用 ArkTS 传的 Callback 通知。
需要管理 Callback 函数引用和异步工作项生命周期。
🤝 模式 3:Promise 异步 - 一言为定!
📜 原理图脑补:
ArkTS主线程(点击按钮) --> [Native Promise接口] --> (立刻返回Promise对象) --> ✅主线程爽滑操作,用.then等结果 ↓ (后台创建异步工作项入队) libuv线程池调度(work子线程) ---> execute回调干活(找图) ---> 主线程EventLoop ---> complete回调 ---> napi_resolve_deferred通知Promise成功,触发.then刷新UI
复制代码
👨💻 Native 接口开发步骤 (MultiThreads.cpp):
// ✨ 1. 导出接口给ArkTS用 (index.d.ts)export const getImagePathAsyncPromise: (imageName: string) => Promise<string>;
// 📦 2. 上下文数据结构 (略改动)struct ContextData { napi_async_work asyncWork = nullptr; napi_deferred deferred = nullptr; // ✨新增!存Deferred对象 // ... (args, result)};
// 🗑️ 3. 销毁上下文函数 (加删Deferred?不用删,napi自己管)
// ⚙️ 4.1 execute回调 (work子线程做,同Callback模式)static void ExecuteFunc([[maybe_unused]] napi_env env, void *data) { ... // 同Callback的ExecuteFunc(干活:找图)}
// ✅ 4.2 complete回调 (主线程执行)static void CompleteFuncPromise(napi_env env, [[maybe_unused]] napi_status status, void *data) { ContextData *ctx = static_cast<ContextData*>(data); // 准备结果值 (图片路径) napi_value resolveValue; napi_create_string_utf8(env, ctx->result.c_str(), ctx->result.length(), &resolveValue);
// 🔥 核心!用Deferred对象通知Promise成功啦! napi_resolve_deferred(env, ctx->deferred, resolveValue);
// 🧹 销毁上下文 DeleteContext(env, ctx);}
// 🧩 5. Native Promise接口实现static napi_value GetImagePathAsyncPromise(napi_env env, napi_callback_info info) { // ... (解析参数:imageName)
// 📦 创建上下文 auto ctxData = new ContextData; ctxData->args = imageName;
// 🪄 创建异步工作项 (同Callback) napi_value asyncName; ... // 略 napi_create_async_work(env, nullptr, asyncName, ExecuteFunc, CompleteFuncPromise, static_cast<void*>(ctxData), &ctxData->asyncWork);
// 🔮 核心!创建Promise对象及其关联的Deferred napi_value promiseObj; napi_create_promise(env, &ctxData->deferred, &promiseObj); // ⭐ctxData->deferred存关键对象
// 🚀 入队异步工作项 napi_queue_async_work(env, ctxData->asyncWork);
return promiseObj; // ⬅️ 把刚创建的Promise对象立刻返回给ArkTS!}
复制代码
🌟 总结 Promise 异步特点:
ArkTS 主线程调用后立刻返回一个Promise对象。
耗时活在 work 子线程(ExecuteFunc)做。
结果在主线程的回调函数(CompleteFuncPromise)中用napi_resolve_deferred通知 Promise 成功,这会触发 ArkTS 的.then。
代码风格更现代(链式调用),逻辑清晰。
🔐 模式 4:线程安全(TSF) - 多线程不打架!
📜 原理图脑补:
ArkTS主线程(点击按钮) --> [Native TSF接口] --> (立刻返回空/null) --> ✅主线程继续爽滑 ↓ (初始化TSF绑定Callback + 后台起线程) 生产者线程🧵 ---> 找图塞队列 消费者线程🧵 ---> 1. 取结果 2. napi_call_threadsafe_function 🚀把结果+回调任务扔给主线程EventLoop ↓ 主线程EventLoop调度 ---> 执行CallJsFunction ---> 调用ArkTS的Callback函数通知结果刷新UI
复制代码
👨💻 Native 接口开发步骤 (MultiThreads.cpp):
// ✨ 1. 导出接口给ArkTS用 (index.d.ts) (和Callback异步签名类似)export const getImagePathAsyncTSF: (imageName: string, callBack: (result: string) => void) => void;
// 📦 2. 全局线程安全函数对象static napi_threadsafe_function tsFun = nullptr; // ⚠️全局!通常是单例!
// 🧩 3.1 CallJs回调(由EventLoop在主线程调度执行)static void CallJsFunction(napi_env env, napi_value jsCallback, [[maybe_unused]] void *context, void *data) { ContextData *ctx = static_cast<ContextData*>(data); // 结果转换 (同Callback completeFunc) napi_value callbackArg; napi_create_string_utf8(env, ctx->result.c_str(), ctx->result.length(), &callbackArg);
// 调用ArkTS的Callback! napi_value undefined; napi_get_undefined(env, &undefined); napi_value ignore; napi_call_function(env, undefined, jsCallback, 1, &callbackArg, &ignore);
// 🧹 销毁传入的上下文数据 DeleteContext(env, ctx);}
// 🧩 3.2 Native TSF接口实现static napi_value GetImagePathAsyncTSF(napi_env env, napi_callback_info info) { // ... (解析参数:imageName, callback)
// 📦 创建上下文 (存参数和结果) auto ctxData = new ContextData; ctxData->args = imageName; // ❗️注意:这个contextData会在CallJsFunction里销毁,不在这里存callbackRef!
// ⚡ 核心1:创建线程安全函数(TSF) (通常首次调用时创建) if (tsFun == nullptr) { constexpr int MAX_QUEUE = 0; // 队列无限大(不阻塞) constexpr int INIT_THREADS = 1; napi_value asyncName; ... // 创建异步资源名略
napi_create_threadsafe_function( // ⭐⭐关键API! env, callbackFunc, // 🔗ArkTS传的Callback函数 nullptr, // async_resource (可空) asyncName, // async_resource_name MAX_QUEUE, // max_queue_size (0=无限) INIT_THREADS, // initial_thread_count nullptr, // thread_finalize_data nullptr, // thread_finalize_cb nullptr, // context CallJsFunction, // 🔥 call_js_cb (关键!主线程调用的函数) &tsFun // 🔥 返回的线程安全函数对象存全局 ); }
// ⚡ 核心2:直接创建后台干活线程 (生产者 & 消费者) // 1. 生产者线程:ProductElement(ctxData) thread producer(ProductElement, static_cast<void*>(ctxData)); producer.detach(); // ❗️非常重要!让线程独立运行,不阻塞当前线程(TSF接口主线程)
// 2. 消费者线程:ConsumeElementTSF(ctxData) (内部会调napi_call_threadsafe_function!) thread consumer(ConsumeElementTSF, static_cast<void*>(ctxData)); consumer.detach(); // ❗️同样detach!
return nullptr; // ⚠️ 立即返回null!}
复制代码
🌟 总结 TSF 异步特点:
ArkTS 调用后立刻返回null。
核心机制是全局的napi_threadsafe_function (tsFun)。
直接在 Native 后台线程(C++线程,非 libuv work 线程)干活! 消费者线程主动调用napi_call_threadsafe_function将任务和结果“推”给主线程 EventLoop。
EventLoop 在主线程调用CallJsFunction,最终调用 ArkTS 的 Callback。
更底层灵活,适用于非 work 子线程的复杂多线程场景。
需要管理全局 TSF 生命周期(通常随模块存在),后台线程detach防止阻塞 TSF 接口调用线程。
🎯 总结:选谁?看场景!
经过这么一大轮学习,咱们最后做个超级清晰的区别对比!存好这张表!🧾
选型小贴士:
能用异步不用同步! 用户体验第一位!🥇
简单异步任务: Callback 或 Promise 都很棒!Promise更现代。
需要精细控制线程或与非 libuv 线程通信: TSF 是不二之选!🧠🔧
结果影响 UI: 确保在 .then() 或 Callback 里更新 UI!否则报错!⚠️
搞定!把这四种模式的玩法彻底搞清楚了!🎉🎉 快去给你的应用加点丝滑的异步魔法吧!✨
评论