一、Worker线程native c++直接给UI线程发送消息
本节写一个让native c++端向UI线程发消息postMessage
的方法。
为了方便测试,最简单事例是UI页面有一个按钮,按下后向Worker线程发消息{type: "NATIVE_POST_MESSAGE"}
让Worker线程调度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线程侧的管理器如下:
//\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线程发送回给UI线程的消息(注意上面按钮是发送给Worker线程的,不是发送给这里的)。
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线程发送出的NATIVE_POST_MESSAGE消息,则调度native c++的post_message_to_ui_thread
方法(这个是自定义的)。
注意在文件末尾声明了export {workerPort}
,不然native调用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++中我们要做相当于在TestWorkerHandler.ets调用workerPort.postMessage({type:"i'm message from native C++", args:11454})
这件事。
在实现之前先声明一个判断变量是否获取成功的方法,单纯靠napi_status判断无法得知调用是否成功:
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;
}
构造消息对象:
//4. 构造一个对象,相当于在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;
}
调用postMessage方法:
//5. 调用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 间接调度ets方法
假设有一个窗口管理模块,暴露出设置窗口大小的接口:
//\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调用是可以异步的,对于一些耗时操作尤其有用。
先定义一个消息类型,当UI线程接收到:
{
type : "RUN_ON_UI_THREAD_JS",
funcName : "WindowUtils.setScreenResolution",
args : [1920, 1080, true]
}
代表要调用方法并传入参数,需要先到线程控制模块中声明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,
};
接收到消息时调用函数:
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;
}
}
调用函数的实现为:
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;
//调用函数
if (!!func) {
let result: ESObject = func(...argsArr);
}
}
}
native中也需要相应改变,在上一节的第四步构造传入函数阶段:
//4. 构造一个对象
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;
}
三、异步操作
假设设置窗口大小是一个耗时操作,我这里用等待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`);
}
}
在消息中我们加入一个新参数timeoutMs用于设置当前操作是否超时,我们假定为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函数处理,否则按一般函数处理:
//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);
}
}
上述逻辑是,设置一个计时的Promise timeout,到达指定时间报错,async方法本身也返回一个Promise命名为succ,Promise.race([succ, timeout])看哪个Promise先执行完毕,没报错执行then,有报错执行catch。
上述写法中我是报log,正常生产环境下,可以调用回调函数。