一、Worker線程native c++直接給UI線程發(fā)送消息
??本節(jié)寫一個讓native c++端向UI線程發(fā)消息postMessage的方法。
??為了方便測試,最簡單事例是UI頁面有一個按鈕,按下后向Worker線程發(fā)消息{type: "NATIVE_POST_MESSAGE"}讓W(xué)orker線程調(diào)度native C++:
//\entry\src\main\ets\pages\Index.ets
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
Row() {
Column() {
Button(('native post return ui thread'))
.width('100%')
.onClick(() => {
TestWorker.getInstance().postMessage({type: "NATIVE_POST_MESSAGE"})
})
}
.width('100%')
}
.height('100%')
}
}
??UI線程側(cè)的管理器如下:
//\entry\src\main\ets\workers\TestWorker.ets
import worker from '@ohos.worker';
import { hilog } from '@kit.PerformanceAnalysisKit';
export class TestWorker {
public threadWorker: worker.ThreadWorker;
private constructor() {
this.threadWorker = new worker.ThreadWorker("entry/ets/workers/TestWorkerHandler.ts");
this.threadWorker.onerror = (e) => {
let msg = e.message;
let filename = e.filename;
let lineno = e.lineno;
let colno = e.colno;
hilog.error(0x0001, "TestWorker", `TestWorker Error ${msg} ${filename} ${lineno} ${colno}`);
};
this.threadWorker.onmessage = (msg) => {
hilog.info(0x0001, "TestWorker", `UIThread get message <${msg.data.type}>`);
}
}
public static getInstance(): worker.ThreadWorker {
if (AppStorage.Get("app_key_test_worker") == null) {
AppStorage.setOrCreate("app_key_test_worker", new TestWorker);
}
let testWorker = AppStorage.Get("app_key_test_worker") as TestWorker;
return testWorker.threadWorker;
}
}
??管理器只是方便單例化管理,外加接收Worker線程發(fā)送回給UI線程的消息(注意上面按鈕是發(fā)送給Worker線程的,不是發(fā)送給這里的)。
??Worker線程定義如下:
//\entry\src\main\ets\workers\TestWorkerHandler.ets
import worker from '@ohos.worker';
import { hilog } from '@kit.PerformanceAnalysisKit';
import testNapi from 'libentry.so';
const workerPort = worker.workerPort
workerPort.onmessage = (e)=> {
hilog.info(0x0001, "TestWorker", `TestWorkerThread get message <${e.data.type}>`);
switch (e.data.type)
{
case "NATIVE_POST_MESSAGE":
testNapi.post_message_to_ui_thread();
break;
}
}
export {workerPort}
??這里判斷假如收到UI線程發(fā)送出的NATIVE_POST_MESSAGE消息,則調(diào)度native c++的post_message_to_ui_thread方法(這個是自定義的)。
??注意在文件末尾聲明了export {workerPort},不然native調(diào)用napi是無法獲取workerPort這個變量的。
??然后聲明接口:
//\entry\src\main\cpp\types\libentry\Index.d.ts
export const post_message_to_ui_thread: () => void;
//\entry\src\main\cpp\napi_init.cpp
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
napi_property_descriptor desc[] = {
{ "post_message_to_ui_thread", nullptr, PostMessageToUIThread, nullptr, nullptr, nullptr, napi_default, nullptr }
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
??在native c++中我們要做相當(dāng)于在TestWorkerHandler.ets調(diào)用workerPort.postMessage({type:"i'm message from native C++", args:11454})這件事。
??在實現(xiàn)之前先聲明一個判斷變量是否獲取成功的方法,單純靠napi_status判斷無法得知調(diào)用是否成功:
bool loadVariantSuccess(napi_env env, napi_value value)
{
napi_valuetype type;
napi_status status = napi_typeof(env, value, &type);
return status == napi_ok && type != napi_undefined;
}
??首先加載文件:
static napi_value PostMessageToUIThread(napi_env env, napi_callback_info info)
{
napi_value result = nullptr;
//1. 使用napi_load_module_with_info加載TestWorkerHandler.ets
napi_value module;
//#include "bundle/native_interface_bundle.h" and link libbundle_ndk.z.so
OH_NativeBundle_ApplicationInfo appInfo = OH_NativeBundle_GetCurrentApplicationInfo();
std::string module_info = std::string(appInfo.bundleName) + "/entry";
free(appInfo.bundleName);
napi_status status = napi_load_module_with_info(env, "entry/src/main/ets/workers/TestWorkerHandler", module_info.c_str(), &module);
if (status != napi_ok)
{
((void)OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, "TestWorker", "load module error"));
return result;
}
??然后從文件中獲取export的workerPort變量:
//2. 獲取文件中定義并export的workerPort變量
napi_value workerPortEtsStr;
std::string funcStr = "workerPort";
napi_create_string_utf8(env, funcStr.c_str(), funcStr.size(), &workerPortEtsStr);
//2. 使用napi_get_property獲取DEFAULT變量
napi_value vWorkerPort;
status = napi_get_property(env, module, workerPortEtsStr, &vWorkerPort);
if (status != napi_ok || !loadVariantSuccess(env, vWorkerPort))
{
((void)OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, "TestWorker", "get property error"));
return result;
}
??從變量中獲取postMessage方法:
//3. 從變量中獲取postMessage方法
napi_value fPostMessage;
status = napi_get_named_property(env, vWorkerPort, "postMessage", &fPostMessage);
if (status != napi_ok || !loadVariantSuccess(env, fPostMessage))
{
((void)OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, "TestWorker", "get function error"));
return result;
}
??構(gòu)造消息對象:
//4. 構(gòu)造一個對象,相當(dāng)于在ets中聲明{type: "i'm message from native C++", args:114514}
napi_value messageObj;
napi_value returnEtsStr;
std::string returnStr = "i'm message from native C++";
napi_create_string_utf8(env, returnStr.c_str(), returnStr.size(), &returnEtsStr);
napi_value args;
napi_create_int32(env, 114514, &args);
const char *keysArray[] = {"type", "args"};
const napi_value valueArray[] = {returnEtsStr, args};
status = napi_create_object_with_named_properties(env, &messageObj, sizeof(keysArray) / sizeof(keysArray[0]), keysArray, valueArray);
if (status != napi_ok)
{
((void)OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, "TestWorker", "create obj error"));
return result;
}
??調(diào)用postMessage方法:
//5. 調(diào)用workerPort.postMessage方法
status = napi_call_function(env, nullptr, fPostMessage, 1, &messageObj, nullptr);
if (status != napi_ok)
{
((void)OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, "TestWorker", "call function error"));
return result;
}
return result;
}
二、Worker native 間接調(diào)度ets方法
??假設(shè)有一個窗口管理模塊,暴露出設(shè)置窗口大小的接口:
//\entry\src\main\ets\utils\WindowUtils.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
export class WindowUtils {
static setScreenResolution(width: number, height: number, fullscreen: boolean) {
hilog.info(0x0001, "TestWorker", `WindowUtils set ${width}x${height} fullscreen:${fullscreen}`);
}
}
??直接export方法自然也是可行的,不過讓ets調(diào)用是可以異步的,對于一些耗時操作尤其有用。
??先定義一個消息類型,當(dāng)UI線程接收到:
{
type : "RUN_ON_UI_THREAD_JS",
funcName : "WindowUtils.setScreenResolution",
args : [1920, 1080, true]
}
??代表要調(diào)用方法并傳入?yún)?shù),需要先到線程控制模塊中聲明map用于交互:
//\entry\src\main\ets\workers\TestWorker.ets
import { WindowUtils } from '../utils/WindowUtils'
export class TestWorker {
public threadWorker: worker.ThreadWorker;
public mModules: Record<string, ESObject> = {
"WindowUtils": WindowUtils,
};
??接收到消息時調(diào)用函數(shù):
this.threadWorker.onmessage = (msg) => {
hilog.info(0x0001, "TestWorker", `UIThread get message <${msg.data.type}>`);
switch (msg.data.type)
{
case "RUN_ON_UI_THREAD_JS":
this.dispatch(msg.data);
break;
}
}
??調(diào)用函數(shù)的實現(xiàn)為:
private dispatch(data: ESObject)
{
const tokens: ESObject = data.funcName.split(".");
//先獲取模塊,既WindowUtils
const mod: ESObject = this.mModules[tokens[0]];
if (!!mod) {
//從模塊中獲取方法setScreenResolution
const func: ESObject = mod[tokens[1]];
let argsArr: Array<ESObject> = data.args;
//調(diào)用函數(shù)
if (!!func) {
let result: ESObject = func(...argsArr);
}
}
}
??native中也需要相應(yīng)改變,在上一節(jié)的第四步構(gòu)造傳入函數(shù)階段:
//4. 構(gòu)造一個對象
napi_value messageObj;
napi_value typeEtsStr;
std::string returnStr = "RUN_ON_UI_THREAD_JS";
napi_create_string_utf8(env, returnStr.c_str(), returnStr.size(), &typeEtsStr);
napi_value funcNameEtsStr;
std::string funcNameStr = "WindowUtils.setScreenResolution";
napi_create_string_utf8(env, funcNameStr.c_str(), funcNameStr.size(), &funcNameEtsStr);
napi_value args;
napi_create_array(env, &args);
napi_value args0;
napi_create_int32(env, 1920, &args0);
napi_value args1;
napi_create_int32(env, 1080, &args1);
const bool trueValue = true;
napi_value args2;
napi_get_boolean(env, trueValue, &args2);
napi_set_element(env, args, 0, args0);
napi_set_element(env, args, 1, args1);
napi_set_element(env, args, 2, args2);
const char *keysArray[] = {"type", "funcName", "args"};
const napi_value valueArray[] = {typeEtsStr, funcNameEtsStr, args};
status = napi_create_object_with_named_properties(env, &messageObj, sizeof(keysArray) / sizeof(keysArray[0]), keysArray, valueArray);
if (status != napi_ok)
{
((void)OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, "TestWorker", "create obj error"));
return result;
}
三、異步操作
??假設(shè)設(shè)置窗口大小是一個耗時操作,我這里用等待5s來模擬:
export class WindowUtils {
static async setScreenResolution(width: number, height: number, fullscreen: boolean) {
hilog.info(0x0001, "TestWorker", `WindowUtils set ${width}x${height} fullscreen:${fullscreen}`);
const sleep = (ms: number) => new Promise<number>(resolve => setTimeout(resolve, ms));
await sleep(5000);
hilog.info(0x0001, "TestWorker", `WindowUtils set resolution end`);
}
}
??在消息中我們加入一個新參數(shù)timeoutMs用于設(shè)置當(dāng)前操作是否超時,我們假定為50ms:
//napi_init.cpp PostMessageToUIThread
napi_value timeoutMs;
napi_create_int32(env, 50, &timeoutMs);
const char *keysArray[] = {"type", "funcName", "args", "timeoutMs"};
const napi_value valueArray[] = {typeEtsStr, funcNameEtsStr, args, timeoutMs};
??在dispatch中判斷,假如等待時間大于等于0,按async函數(shù)處理,否則按一般函數(shù)處理:
//TestWork.ets private async dispatch(data: ESObject)
if (!!func)
{
if (data.timeoutMs >= 0)
{
let timeout: Promise<number> = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Async call timeout"));
}, data.timeoutMs);
});
let succ: ESObject = func(...argsArr);
Promise.race([succ, timeout]).then((result: ESObject) => {
hilog.info(0x0001, "TestWorker", `UI Thread async success`);
}).catch((reason: ESObject) => {
hilog.error(0x0001, "TestWorker", `UI Thread async timeout`);
});
}
else
{
let result: ESObject = func(...argsArr);
}
}
??上述邏輯是,設(shè)置一個計時的Promise timeout,到達指定時間報錯,async方法本身也返回一個Promise命名為succ,Promise.race([succ, timeout])看哪個Promise先執(zhí)行完畢,沒報錯執(zhí)行then,有報錯執(zhí)行catch。
??上述寫法中我是報log,正常生產(chǎn)環(huán)境下,可以調(diào)用回調(diào)函數(shù)。