• 作者:老汪软件技巧
  • 发表时间:2024-08-27 10:03
  • 浏览量:

void ShapeWrap::onRegister(napi_env env, napi_value exports) {
  napi_status status;
  napi_value constructor;
  status = napi_define_class(env, ShapeWrap::S_CLASS.c_str(), NAPI_AUTO_LENGTH, ShapeWrap::CreateJsObject, nullptr, 0,
                             nullptr, &constructor);
  if (status != napi_ok) {
    // 处理错误
    return;
  }
  napi_property_descriptor properties[] = {
    {"compareArea", nullptr, ShapeWrap::compareArea, nullptr, nullptr, nullptr, napi_default, nullptr}};
  napi_define_properties(env, constructor, sizeof(properties) / sizeof(properties[0]), properties);
  napi_set_named_property(env, exports, S_CLASS.c_str(), constructor);
}

napi_create_object(env, &ret);
napi_wrap(
  env, ret, (void *)shapeWrap,
  [](napi_env env, void *finalize_data, void *finalize_hint) {
    ShapeWrap *shapeWrap = (ShapeWrap *)finalize_data;
    delete shapeWrap;
  },
  nullptr, nullptr);
napi_property_descriptor properties[] = {
  {"getArea", nullptr, ShapeWrap::getArea, nullptr, nullptr, nullptr, napi_default, nullptr}};
napi_define_properties(env, ret, sizeof(properties) / sizeof(properties[0]), properties);
return ret;

主要区别就是静态方法是把属性挂到constructor上面。成员方法是把属性挂到创建的对象上

答疑:为什么设计一个桥接类?如果不设计桥接类,直接在napi_init.cpp中写逻辑是否可以?

桥接类的意义是作为c++和ts之间对象和方法、回调的互转,作为原生c++代码的ts适配层,避免原生代码中嵌入ts相关的功能,导致整体代码逻辑不够清晰。

直接在napi_init.cpp中写逻辑也是可以的,但是如果涉及需要转换的对象和方法非常多,napi_init.cpp就会非常臃肿,而且逻辑很难看清。所以建议是采用桥接类方式,同时每个类的注册流程放入自己的类内部。这样逻辑清晰,不会因为修改某一个类而影响其他类的初始化逻辑。

接口的定义只能定义静态方法,怎么变成对象方法呢?

  napi_value ShapeWrap::getArea(napi_env env, napi_callback_info info) {
    napi_value thisArg;
    napi_get_cb_info(env, info, nullptr, nullptr, &thisArg, nullptr);
    ShapeWrap *shapeWrap = nullptr;
    napi_unwrap(env, thisArg, (void **)&shapeWrap);
    if (shapeWrap == nullptr) {
      return nullptr;
    }
    int area = shapeWrap->getShape()->getArea();
    napi_value ret;
    if (napi_create_int32(env, area, &ret) != napi_ok) {
      return nullptr;
    }
    return ret;
  }

以这个例子为例,getArea本身是个静态方法,但是我们通过wrap一个c++对象指针在这个Js对象上,所以我们可以通过上下文获取到当前调用的Js对象,从里面取出对应的c++对象。然后把静态方法的参数调用到这个c++对象的对象方法,从而实现静态方法到对象方法的转换。

对象传递

一般在多语言开发中会涉及把ts对象传递给c/c++层,或者把c++层对象传递给ts层两种场景。分别说明处理方式。

ts传递对象到native

ts的成员函数调用,native可以通过

    napi_value thisArg;
    napi_get_cb_info(env, info, nullptr, nullptr, &thisArg, nullptr);

获取当前的调用对象,然后通过napi的api方法获取调用对象内的各种属性。

native传递对象到ts

传递js对象通过napi_create_object可以c++层创建js对象,然后通过返回值方式返回js对象。

方法调用

js调用c方法,直接使用c层暴露的接口即可,

c层调用js层,需要使用napi的napi_call_function方法,去调用js层对象或者静态方法。

c层创建js对象返回给js层

上面例子中shape1,和shape2就是c层创建的js对象,返回给js层。可以参考以上例子。

js创建c层对象

js层创建c层对象,直接通过方法调用,c层直接创建出c层对象,主要难点是c层的对象指针如何保存的问题。有几种处理方式,最推荐的还是使用napi的wrap方式来处理。通过napi wrap来把c层的对象指针包裹在一个js对象上,同时把c层对象的生命周期也绑定到js对象上,当js对象销毁时,直接调用c层对象delete函数去释放对象。

如上例子中:

std::unique_ptr shape = std::make_unique(w, h);
ShapeWrap *shapeWrap = new ShapeWrap(std::move(shape));
napi_create_object(env, &ret);
napi_wrap(
  env, ret, (void *)shapeWrap,
  [](napi_env env, void *finalize_data, void *finalize_hint) {
    ShapeWrap *shapeWrap = (ShapeWrap *)finalize_data;
    delete shapeWrap;
  },
  nullptr, nullptr);

当创建napi_wrap时,把shapeWrap的指针传递进去,同时在js销毁的回调中,去把存入的指针转换为正确的类型,并通过delete调用析构函数释放内存。

还有其他一些方案,通过把c++层的对象指针进行hex编码传递到js层保存,js层主动调用c层方法传递hex编码去释放对象,c层通过hex解析出对象地址,然后释放内存。

hex指针转换方法例子

std::string NapiUtils::PtrToString(void *ptr) {
    std::stringstream ss;
    ss << std::hex << std::showbase << reinterpret_cast<uintptr_t>(ptr);
    return ss.str();
}
// 将字符串形式的唯一标识符转换回指针
void *NapiUtils::StringToPtr(const std::string &str) {
    uintptr_t ptr;
    std::istringstream iss(str);
    iss >> std::hex >> ptr;
    return reinterpret_cast<void *>(ptr);
}

这种方式的优缺点如下,相比wrap的方式,不推荐这种方式。

优点:处理简单

缺点:c层对象的生命周期需要js显示的去管理和释放,比较容易出现内存泄漏。

c层创建js对象,

c层直接通过napi_create_object可完成js对象的创建。

js通过js对象调用c层方法

如上例中,

let area = shape1.getArea()

如何开发鸿蒙app_鸿蒙开发js_

即是js对象通过js对象调用c层方法的方式。具体可以参考例子。

c++导出类和方法到js层

可参考上面例子中Shape类的导出和静态方法以及成员方法的导出。

对象生命周期管理c++对象生命周期管理

js与c++交互中,c++对象的生命周期管理尤为重要,因为使用不当很容易造成内存泄漏。推荐使用napi_wrap方式,并且一定要写释放回调,在回调用去delete传入的c++对象指针,这样把c++对象生命周期与js对象绑定,能够防止出现忘记调用delete c++对象的问题。

std::unique_ptr shape = std::make_unique(w, h);
ShapeWrap *shapeWrap = new ShapeWrap(shape);
napi_create_object(env, &ret);
napi_wrap(
  env, ret, (void *)shapeWrap,
  [](napi_env env, void *finalize_data, void *finalize_hint) {
    ShapeWrap *shapeWrap = (ShapeWrap *)finalize_data;
    delete shapeWrap;
  },
  nullptr, nullptr);

当进行 N-API 调用时,引擎堆中的对象句柄会作为 napi_value 返回,这些句柄控制着对象的生命周期。默认情况下,对象的句柄与其所在的 native 方法的作用域一致。然而,在实际开发中,可能需要对象有比当前 native 方法更短或更长的作用域。

js对象的生命周期管理缩短对象生命周期

为了最小化对象的生命周期并避免内存泄漏问题,开发者可以通过合理使用 napi_open_handle_scope 和 napi_close_handle_scope 来管理对象。

举例来说,考虑一个带有 for 循环的方法,该循环遍历一个大型数组的元素:

for (int i = 0; i < 1000000; i++) {
 napi_value result;
 napi_status status = napi_get_element(env, object, i, &result);
 if (status != napi_ok) {
  break;
 }
 // do something with element
}

在 for 循环中会创建大量的 handle,消耗大量资源。为了减小内存开销,N-API 提供创建局部 scope 的能力,在局部 scope 中间所创建 handle 的生命周期将与局部 scpoe 保持一致。一旦不再需要这些 handle,就可以直接关闭局部 scope。

例如,使用下面的方法,可以确保在循环中,最多只有一个句柄是有效的:

// 在for循环中频繁调用napi接口创建js对象时,要加handle_scope及时释放不再使用的资源;
// 下面例子中,每次循环结束局部变量res的生命周期已结束,因此加scope及时释放其持有的js对象,防止内存泄漏。
for (int i = 0; i < 1000000; i++) {
    napi_handle_scope scope;
    napi_status status = napi_open_handle_scope(env, &scope);
    if (status != napi_ok) {
        break;
    }
    napi_value result;
    status = napi_get_element(env, object, i, &result);
    if (status != napi_ok) {
        break;
    }
    // do something with element
    status = napi_close_handle_scope(env, scope);
    if (status != napi_ok) {
        break;
    }
}

以上例子,循环中创建了很多个result的局部变量,因为scope的控制关系,没轮循环,前一轮的result都会及时的释放掉。

存在一些场景,某些对象的生命周期需要大于对象本身所在区域的生命周期,例如嵌套循环场景。开发者可以通过 napi_open_escapable_handle_scope 与 napi_close_escapable_handle_scope 管理对象的生命周期,在此期间定义的对象的生命周期将与父作用域的生命周期保持一致。

延长对象生命周期

在一些场景中需要延长js对象的声明周期,比如在c/c++的回调函数中,需要把回调结果传递给js层的方法,产生js的回调,这就需要js的回调在c/c++的回调生命周期中一直有效,不能被回收掉。

可以通过 napi_create_reference创建 napi_ref 来延长 napi_value 对象的生命周期,创建的对象需要用户手动调用 napi_delete_reference 释放,否则可能造成内存泄漏。有个简单方法是创建一个c++类,在构造函数调用napi_create_reference,在析构函数调用napi_delete_reference,成员变量存储一个napi_ref,这样c++对象的声明周期就与napi_create_reference创建的js对象关联上,管理好这个c++对象生命周期就可以了。

例子:

class ResourceCallback{
  public:
  ResourceCallback(napi_env env, napi_value resourceCallbackJs);
  ResourceCallback() = delete;
  ~ResourceCallback();
private:
  napi_env m_Env;
  napi_ref m_ResourceCallbackJs;
};

ResourceCallback::ResourceCallback(napi_env env, napi_value resourceCallbackJs) : m_Env(env) {
  napi_create_reference(env, resourceCallbackJs, 1, &m_ResourceCallbackJs);
  }
  
  ResourceCallback::~ResourceCallback() {
  napi_delete_reference(m_Env, m_ResourceCallbackJs);
  }

异步操作

napi中主要异步主要有两种方式实现。

Threadsafe Function(线程安全函数):

AsyncWorker(异步工作者):

例子:

#include 
#include 
#include  // 用于 sleep 函数的头文件
// 定义异步工作者结构体
typedef struct {
    napi_env env;          // N-API 环境
    napi_ref callback_ref; // JavaScript 回调函数的引用
    // 其他需要的数据字段可以在这里添加
    int result;            // 存储结果的字段
} AsyncData;
// 异步工作者执行的任务函数
void ExecuteAsyncWork(napi_env env, void* data) {
    AsyncData* async_data = (AsyncData*)data;
    
    // 模拟一个耗时操作
    sleep(3);
    
    // 设置结果字段
    async_data->result = 42;
}
// 异步工作者任务完成后的回调函数
void CompleteAsyncWork(napi_env env, napi_status status, void* data) {
    AsyncData* async_data = (AsyncData*)data;
    
    // 创建回调函数的 JavaScript 参数
    napi_value callback;
    napi_get_reference_value(env, async_data->callback_ref, &callback);
    
    // 创建返回给 JavaScript 的结果值
    napi_value result;
    napi_create_int32(env, async_data->result, &result);
    
    // 调用 JavaScript 回调函数
    napi_call_function(env, NULL, callback, 1, &result, NULL);
    
    // 释放回调函数的引用
    napi_delete_reference(env, async_data->callback_ref);
    
    // 释放异步数据的内存
    free(async_data);
}
// JavaScript 调用的异步函数
napi_value MyAsyncFunction(napi_env env, napi_callback_info info) {
    // 解析 JavaScript 回调函数
    size_t argc = 1;
    napi_value argv[1];
    napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
    
    // 创建异步数据结构体
    AsyncData* async_data = (AsyncData*)malloc(sizeof(AsyncData));
    async_data->env = env;
    
    // 保存 JavaScript 回调函数的引用
    napi_create_reference(env, argv[0], 1, &(async_data->callback_ref));
    
    // 创建异步工作者
    napi_async_work async_work;
    napi_create_async_work(env, NULL, "MyAsyncWork", ExecuteAsyncWork, CompleteAsyncWork, async_data, &async_work);
    
    // 执行异步工作者
    napi_queue_async_work(env, async_work);
    
    // 返回 undefined
    napi_value undefined;
    napi_get_undefined(env, &undefined);
    return undefined;
}
// 模块初始化函数
napi_value Init(napi_env env, napi_value exports) {
    // 定义并注册 JavaScript 函数
    napi_value fn;
    napi_create_function(env, NULL, 0, MyAsyncFunction, NULL, &fn);
    napi_set_named_property(env, exports, "myAsyncFunction", fn);
    return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

解释说明:

异步数据结构体和任务函数:

异步工作者完成后的回调函数:

JavaScript 调用的异步函数:

模块初始化和导出函数:

线程安全线程函数使用注意事项性能相关跨语言调用开销接口调用数值转换

使用 N-API 进行 ArkTS 与 C++ 之间的数据转换,

有如下建议:

*减少数据转换次数:频繁的数据转换可能会导致性能下降,可以通过批量处理数据或者使用更高效的数据结构来优化性能;

避免不必要的数据复制:在进行数据转换时,可以使用 N-API 提供的接口来直接访问原始数据,而不是创建新的数据副本;合理的使用c++的move通过语义转移的方式,尽量减少不必要的拷贝;

使用缓存:如果某些数据在多次转换中都会被使用到,可以考虑使用缓存来避免重复的数据转换。缓存可以减少不必要的计算,提高性能。