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)。