01
前言
鸿蒙作为一款新兴的智能操作系统,现在适配鸿蒙系统的应用越来越多,同时会面临三端兼容问题,如同一产品功能,需要维护iOS、Android、鸿蒙三端代码。
拿文件上传、下载功能场景举例,同时要适配iOS、Android、鸿蒙三端,目前比较好的方案是文件上传、下载功能下沉到C,三端native代码可以复用,但是在鸿蒙上存在如下一个问题:
Native文件上传、下载进度怎么从Native线程异步回调给ArkTs线程对应JS回调方法?
答案:通过Node-API提供的线程安全函数,可以把文件上传、下载进度从native线程异步回调ArkTs线程JS回调方法。

02
Node-API是什么
Node-API(以前称为 N-API)是用于构建native功能的API,它独立于底层的JavaScript引擎(例如 V8引擎),并作为 Node.js 本身的一部分进行维护。

Node.js是JavaScript运行时环境,用来支持JS代码的执行,HarmonyOS Native API对Node-API的接口进行了封装和重写,支持Node-API标准款的部分接口,以此提供ArkTS/JS与C/C++模块之间交互能力。
Node-API必须遵循一个原则: Node-API的调用必须和JS的调用线程一致。
1)Node-API接口只能在JS线程使用,JS调用Native的线程才能调用Node-API接口,Native子线程无法调用;
2)napi env是无法跨线程使用,如果使用全局变量将napi env保存下来,再在其他线程使用会崩溃。
上面提到的文件上传场景,文件上传是在native子线程,非JS线程,如果使用Node-API,必然会崩溃,但Node-API提供线程安全函数,通过线程安全函数可以在native线程里回调JS线程。
03
线程安全函数如何实现跨线程通信
3.1 线程安全函数
线程安全函数ThreadSafeFunction(简称TFS)为了解决跨线程的函数调用问题,可以将普通的JS函数封装成线程安全函数,包装之后的函数是允许在线程之间传递,线程安全函数的定义如下:
NAPI_EXTERN napi_status
napi_create_threadsafe_function(napi_env env,napi_value func,napi_value async_resource,napi_value async_resource_name,size_t max_queue_size,size_t initial_thread_count,void* thread_finalize_data,napi_finalize thread_finalize_cb,void* context,napi_threadsafe_function_call_js call_js_cb,napi_threadsafe_function* result);
参数 | 解释 |
---|---|
env | 调用API的环境 |
func | JS回调函数 |
async_resource | 与将传递给可能的async_hooks init钩子的异步工作关联的可选对象 |
async_resource_name | 一个JS字符串,用于为 async_hooks API提供的资源类型提供标识符 |
max_queue_size | 事件队列的最大大小,0为无限制 |
initial_thread_count | 初始线程数,包括将使用此函数的主线程 |
thread_finalize_data | 要传递给 thread_finalize_cb的可选数据 |
thread_finalize_cb | napi_threadsafe_function被销毁时调用的可选函数 |
context | napi_threadsafe_function上下文 |
call_js_cb | 可选回调,调用JS函数以响应不同线程上的调用,此回调将在主线程上调用 |

使用TFS一般分为如下几步:
step1:native侧创建线程安全函数,绑定ArkTs的API的callback和线程安全回调函数call\_js\_cb;step2:native子线程执行异步任务,并在子线程中调用napi_call_threadsafe_function,将call_js_cb抛到事件循环中进行调度;
step3:在call_js_cb中通过调用napi_call_function将native异步任务的结果回调给ArkTs线程,进行相关的UI刷新操作。
3.2 EventLoop事件循环
Libuv是由C语言编写的,基于事件驱动的异步I/O库,利用编译好的libuv库文件,可以写一个简单事件驱动的例子:
#include "stdio.h"
#include "uv.h"int main() {uv_loop_t *loop = uv_default_loop(); printf("hello libuv");uv_run(loop, UV_RUN_DEFAULT);
}

线程安全函数是怎么实现主线程、线程之间通信?
libuv通过循环不断取出watcher队列中的事件,uv__iot_t结构体保存了文件描述符,其对应一个事件和事件回调,通过uv__iot_t结构体来初始化 epoll_event,再使用epoll_wait来等待文件描述符上I/O事件,事件触发之后调用对应的回调函数,通过epoll实现线程间的通信。
API | 解释 |
---|---|
uv_run(uv_loop_t* loop, uv_run_mode mode) | 运行事件循环,有如下几种mode: |
uv_loop_alive(const uv_loop_t* loop) | 如果有被引用的活动句柄、活动请求或者循环里的关闭句柄时返回非零值,否则返回零值,表示事件不再活动 |
uv__io_t | IO观察者,是一个结构体,描述了上层事件和事件回调信息 |
uv__io_poll(loop, timeout) | 把新增需要被监听的fd放到poll中,其内部有一个epoll_wait方法 |
uv_async_start、uv__io_start | 注册IO观察者到事件loop里,并注册需要监听的事件 |
uv__async_send、uv_async_send | 触发事件执行,并执行事件的回调 |
04
鸿蒙搜狐新闻文件上传场景Native侧调用ArkTS侧的系统能力实现
下面以鸿蒙搜狐新闻时间线视频、图片发布场景举例,如何在native子线程上传视频、图片,并把上传进度、上传错误、上传完成状态通过线程安全函数回调给ArkTS-UI线程,进行UI上传进度更新等刷新UI操作。

4.1 native文件上传调用ArkTs JS方法整体设计
鸿蒙搜狐新闻文件上传、下载下沉到native,网络请求使用了第三方库curl,签名计算算法使用了openssl库,将curl、openssl编译成so或者静态a文件并实现文件上传、下载功能。

鸿蒙搜狐新闻Native文件上传通过线程安全函数调用ArkTs JS方法设计流程图如下:

4.2 native文件上传调用ArkTs JS方法整体设计详细实现
native调用ArkTs JS方法效果图

创建文件上传线程安全函数是在ArkTs线程执行,调用文件上传进度线程安全函数是在Native子线程,执行文件上传进度回调JS方法是在ArkTs线程执行。

API方法实现
从上往下方法调用链如下:
uploadFile方法定义:
JS回调方法 | 解释 |
---|---|
onProgressFun | 文件上传进度回调 |
onCompleteFun | 文件上传完成回调 |
onErrorFun | 文件上传错误回调 |
/*** 文件上传** @param uploadUrl 上传url* @param filePath 本地文件路径* @param onProgressFun 上传进度回调 0~100* @param onCompleteFun 上传完成回调* @param onErrorFun 错误回调,errorCode:错误code,errorDesc:错误描述*/
export const uploadFile: (uploadUrl: string, filePath: string,onProgressFun: (progress: number) => void, onCompleteFun: () => void, onErrorFun:(errorCode: number, errorDesc: string) =>void) => void;
鸿蒙Native上传文件方法定义:
static napi_value NAPI_Global_uploadFile(napi_env env, napi_callback_info info) {size_t argc = 5;napi_value args[5] = {nullptr, nullptr, nullptr, nullptr, nullptr};... // 省略参数解析代码实现 MultiFileManager::getInstance()->uploadFileWrapper(uploadUrl, filePath, env, args[2], args[3], args[4]);return nullptr;
}/*** 上传文件子线程执行** @param uploadUrl* @param filePath*/
void MultiFileManager::uploadFileWrapper(std::string uploadUrl, std::string filePath, napi_env env, napi_value onProgressFun,napi_value onCompleteFun, napi_value onErrorFun){UploadBridge::createThreadUploadCallBack(env,onProgressFun, onCompleteFun, onErrorFun);pool->enqueue([this](std::string uploadUrl, std::string filePath) { uploadFile(uploadUrl, filePath); },uploadUrl, filePath);
}
在上传任务放到native子线程执行之前,先在ArkTs线程里创建线程安全函数,绑定ArkTs的上传方法onProgressFun回调和线程安全回调函数callJsUploadOnProgress。
创建上传文件进度线程安全函数定义:
createThreadUploadCallBack方法说明:
native线程执行文件上传进度callUploadOnProgress方法后,napi_call_threadsafe_function会调用文件上传进度线程安全函数,即底层会调用uv_async_send将JS文件上传进度回调方法callJsUploadOnProgress抛到EventLoop事件循环中执行
事件调度在ArkTs线程执行callJsUploadOnProgress方法,并回调JS的onProgressFun方法,ArkTs UI拿到上传文件进度后更新UI
/*** 创建线程安全函数* * @param env* @param onProgressFun* @param onCompleteFun* @param onErrorFun*/
void UploadBridge::createThreadUploadCallBack(napi_env env, napi_value onProgressFun, napi_value onCompleteFun, napi_value onErrorFun){uploadProgress = -1;napi_value workName;napi_create_string_utf8(env, "workItem", NAPI_AUTO_LENGTH, &workName);// 创建线程安全函数napi_create_threadsafe_function(env, onProgressFun, NULL, workName, 0, 1, NULL, NULL, NULL, callJsUploadOnProgress, &onProgressTsFn);......
}
ArkTs线程执行JS上传文件进度方法定义:
/*** JS文件上传进度回调方法* * @param env* @param js_cb* @param context* @param data*/
void UploadBridge::callJsUploadOnProgress(napi_env env, napi_value jsCallBack, void *context, void *data) {napi_value argv;napi_create_int32(env, uploadProgress, &argv);napi_value result = nullptr;napi_call_function(env, nullptr, jsCallBack, 1, &argv, &result);......
}
native线程调用文件上传进度安全函数方法定义:
/*** 上传文件** @param fileMeta*/
void MultiFileRequest::uploadFile(UploadFileMeta *fileMeta) {CURLcode ret;CURL *curlHandle;std::string response;curl_global_init(CURL_GLOBAL_ALL);curlHandle = curl_easy_init();if (curlHandle == NULL) { // 初始化失败UploadBridge::callUploadOnError(CURL_INIT_ERROR, getErrorDesc(CURL_INIT_ERROR));return;}curlHandle = appendTestProxy(curlHandle);......fileMeta->uploadInfo.file = uploadFile;fileMeta->uploadInfo.uploadedSize = fileMeta->uploadedSize;;fileMeta->uploadInfo.totalSize = fileMeta->totalSize;curl_easy_setopt(curlHandle, CURLOPT_UPLOAD, 1L);curl_easy_setopt(curlHandle, CURLOPT_PUT, 1l);curl_easy_setopt(curlHandle, CURLOPT_HTTPHEADER, headers);curl_easy_setopt(curlHandle, CURLOPT_URL, fileMeta->url.c_str());// 传入文件上传进度回调方法curl_easy_setopt(curlHandle, CURLOPT_READFUNCTION, MultiFileRequest::uploadReadData);......curl_slist_free_all(headers);fclose(uploadFile);curl_easy_cleanup(curlHandle);curl_global_cleanup();
}/*** 读取上传文件二进制数据**/
size_t MultiFileRequest::uploadReadData(void *ptr, size_t size, size_t nMem, void *userp) {UploadInfo *uploadInfo = static_cast<UploadInfo *>(userp);unsigned long nread;size_t retcode = fread(ptr, size, nMem, uploadInfo->file);if (retcode > 0) {nread = (unsigned long)retcode;uploadInfo->uploadedSize += nread;}if (uploadInfo->totalSize > 0) {// 文件上传过程中读文件可能会出错,会从文件的某个offset重新上传,导致已上传文件大小大于文件总大小的情况,避免出现进度大于100%的情况if (uploadInfo->uploadedSize <= uploadInfo->totalSize) { UploadBridge::callUploadOnProgress((float)uploadInfo->uploadedSize / uploadInfo->totalSize * 100);} else {Utils::logD ("retry uploadFile");}}return retcode;
}/*** antive文件上传进度回调方法* * @param progress*/
void UploadBridge::callUploadOnProgress(int progress) {Utils::logD("上传文件进度是:" + std::to_string(progress));if (uploadProgress != progress) {uploadProgress = progress;napi_acquire_threadsafe_function(onProgressTsFn);napi_call_threadsafe_function(onProgressTsFn, NULL, napi_tsfn_nonblocking);}
}
05
最后
在Native侧调用ArkTS侧的系统能力,除了使用线程安全函数外,还可以直接使用libuv,但需要额外编译libuv源码,如果需要了解更多libuv的知识,可以参见(https://github.com/libuv/libuv)。