OpenHarmony napi异步调用ets的方法

一、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,正常生产环境下,可以调用回调函数。

引用:https://developer.huawei.com/consumer/cn/doc/best-practices-V5/bpta-complex-type-pass-V5#section1521193811224

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容