【鸿蒙实战开发】native开发入门指南

31 篇文章 0 订阅
31 篇文章 0 订阅

背景

鸿蒙最近开始推进纯血鸿蒙OS,纯血鸿蒙OS顾名思义,就是不再兼容安卓的framwork层。基于安卓平台开发的apk将在纯血鸿蒙OS上不再支持运行。所以各大主流软件都开始开发纯血鸿蒙OS的应用。鸿蒙ui层语言使用的是js,js的执行效率相对比较低效,所以鸿蒙官方也推荐使用native方式实现一些高性能需求,鸿蒙上UI框架语言使用的是前端技术栈的ts语法的js代码。所以对于跨平台的代码需要重新设计关于native的交互部分。本篇文章主要探索鸿蒙native的开发方法。

现状

前端技术栈中,nodejs与native交互主要通过N-API的方式,N-API(Node.js API)是 Node.js 提供的一套 C/C++ 编写 Node.js 模块的应用程序接口。它允许开发者编写跨平台的、与 Node.js 版本无关的本地插件,而无需担心不同版本 Node.js 的兼容性问题。

鸿蒙的UI框架的技术栈采用的就是nodejs的技术栈,所以鸿蒙上与native的交互主要也是依赖于N-API的接口。鸿蒙上对外提供的N-API的接口主要集中在以下两个头文件中。只要把这两个头文件中涉及的方法理解透彻就能得心应手的掌握鸿蒙与native交互的方法。

#include <js_native_api.h>

#include <js_native_api_types.h>

下面将详细结合具体例子介绍鸿蒙上基于N-API的native交互设计方式方法。

工程搭建

鸿蒙上创建native开发环境有两种方式:

  1. 创建一个全新的应用,其中包含native模块

使用鸿蒙开发IDE,new->Create Project方式创建

image.png

选择Native c++项目

项目文件结构如图:

image.png

  1. 在已有项目中增加native的开发模块,native作为独立模块

类似android中aar。这种创建方式是在IDE的选择已有项目,new->module方式创建

image.png

可以选择其中标红的两种,不能选择其他。

其中两种的区别如下:

1) Shared Library:

这种方式是动态库形式,其中native代码会打包成so包裹在独立的.har后缀的文件中,在调用的主项目打包时,并不会把Shared Library的.har文件打入自己的.hap中,而只是在运行时去动态加载.har文件,可以同时被多个module使用,同时只有一份库文件,跟linux上的so机制很相似,这种模式打包的同时也会生成.hsp文件。

2) Static Library:

这种方式是静态库形式,其中native代码也会打成har文件,但其中是静态库形式,在跟主项目链接阶段会统一打包到主项目.hap发布文件中。

在IDE中生成module时注意打开Enable native,保证module生成基础的native开发配置文件。

image.png

三种设备类型

Phone:手机

Tablet:华为的平板

2in1:华为的安装鸿蒙系统的pc

module方式与独立应用方式的目录结构基本类似。

image.png

到此工程的搭建工作就基本完成。

框架设计

接口设计

natvie接口设计中一般有两种方式,一种是方法接口,直接提供静态方法给ts层调用。另一个种是导出类给ts层调用,两种方式都是在实际应用中较常使用的。

方法接口

创建工程时,默认创建的demo就是这种接口方式。

ts中接口声明如下

image.png

直接在接口声明前加上export即可。

把so与接口声明的ts文件在oh-package.json5中关联上

image.png

在c中,通过napi_module_register注册函数把c函数注册上即可。

image.png

类接口

类接口的方式是在c/c++层定义ts类,可以在c/c++层创建ts对象,并且传递ts对象到ts层。类中一般可以定义两种函数,静态函数和成员函数。通过c/c++同样可以定义ts类的这两类函数。下面我们用一个例子来解释如何使用。

一个Shape类。其中包含两个方法 ,一个成员方法int getArea()获取区域的面积,一个静态方法static bool compareArea(const Shape& shape1, const Shape& shape2)比较两个区域的面积。我们构建一个ts的Shape类,并且定义这个类的以上两个方法,并且方法的实现放在native层。

其中包含一个成员方法:


int getArea() const;

一个静态方法:

static bool compareArea(const Shape &shape1, const Shape &shape2);

我们先构建c++类

#ifndef MYAPPLICATION_SHAPE_H
#define MYAPPLICATION_SHAPE_H
#include <sys/stat.h>
namespace ttsample {
 class Shape {

 public:
 // 静态方法:比较两个形状的面积
 static bool compareArea(const Shape &shape1, const Shape &shape2);

 Shape() = delete;
 Shape(int w, int h) : width(w), height(h) {}

 // 成员方法:计算面积
 int getArea() const;

 private:
 int width;
 int height;
 };
} // namespace ttsample
#endif // MYAPPLICATION_SHAPE_H
#实现cpp
#include "shape.h"

namespace ttsample {
 bool Shape::compareArea(const Shape &shape1, const Shape &shape2) { return shape1.getArea() > shape2.getArea(); }

 int Shape::getArea() const { return width * height; }
} // namespace ttsample

再构建桥接类,桥接类的意义是把原生c++与js层类型和方法和回调进行桥接

#ifndef MYAPPLICATION_SHAPE_WRAP_H
#define MYAPPLICATION_SHAPE_WRAP_H
#include <memory>
#include <string>
#include "napi/native_api.h"
#include "sample/shape.h"
namespace ttbridge {
 class ShapeWrap {
 public:
 static void onRegister(napi_env env, napi_value exports);
 static napi_value CreateJsObject(napi_env env, napi_callback_info info);
 static napi_value getArea(napi_env env, napi_callback_info info);
 static napi_value compareArea(napi_env env, napi_callback_info info);
 static std::string S_CLASS;
 ShapeWrap(std::unique_ptr<ttsample::Shape> &&shape);
 ShapeWrap() = delete;
 ~ShapeWrap();
 ttsample::Shape *getShape() { return m_Shape.get(); }

 private:
 std::unique_ptr<ttsample::Shape> m_Shape;
 };
} // namespace ttbridge
#endif

桥接类实现

#include "shape_wrap.h"
#include "sample/shape.h"
#include "utils/napi_utils.h"

namespace ttbridge {
 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_value ShapeWrap::CreateJsObject(napi_env env, napi_callback_info info) {
 napi_value ret;
 std::vector<napi_value> args = NapiUtils::GetArgs(env, info);
 if (args.size() < 2) {
 return nullptr;
 }
 int w = 0;
 int h = 0;
 napi_get_value_int32(env, args[0], &w);
 napi_get_value_int32(env, args[1], &h);

 std::unique_ptr<ttsample::Shape> shape = std::make_unique<ttsample::Shape>(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_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;
 }

 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;
 }

 napi_value ShapeWrap::compareArea(napi_env env, napi_callback_info info) {
 std::vector<napi_value> args = NapiUtils::GetArgs(env, info);
 if (args.size() < 2) {
 return nullptr;
 }
 void *shape1 = nullptr;
 void *shape2 = nullptr;

 napi_unwrap(env, args[0], &shape1);
 napi_unwrap(env, args[1], &shape2);
 ttsample::Shape *shape1Ptr = (ttsample::Shape *)shape1;
 ttsample::Shape *shape2Ptr = (ttsample::Shape *)shape2;
 bool ret = ttsample::Shape::compareArea(*shape1Ptr, *shape2Ptr);
 napi_value retJs;
 napi_get_boolean(env, ret, &retJs);
 return retJs;
 }

 std::string ShapeWrap::S_CLASS = "ShapeWrap";

 ShapeWrap::ShapeWrap(std::unique_ptr<ttsample::Shape> &&shape) : m_Shape(shape)) {}

 ShapeWrap::~ShapeWrap() {}

}

注意:函数的桥接也需要定义一个相似的静态函数,因为ts侧只能通过静态函数桥接,而对象是通过静态函数的参数传递。如下:


static napi_value getArea(napi_env env, napi_callback_info info);

调用对象是存储在info中,需要通过如下方法获取调用对象。thisArg中即是ts层的类对象。可以从其中获取我们包装的c++对象,从而实现调用c++成员方法的目的。

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

通过unwrap获取创建对象时wrap上的c++对象

 ShapeWrap *shapeWrap = nullptr;
 napi_unwrap(env, thisArg, (void **)&shapeWrap);
 if (shapeWrap == nullptr) {
 return nullptr;
 }

为了保证桥接类的生效,还需要在so初始化方法中记得注册桥接类。

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
 napi_property_descriptor desc[] = {{"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr}};
 napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);

 ttbridge::ShapeWrap::onRegister(env, exports);
 return exports;
}
EXTERN_C_END

ts层对应定义

在index.d.ts中定义如下:


export declare class Shape{
 constructor(w:Number, h:Number)
 getArea():Number
 static compareArea(shape1:Shape, shape2:Shape):boolean
}

以上定义说明声明了一个类Shape,同时声明了三个方法,算上构造函数。但是方法的具体定义和实现是在native编译的so中。

至此,ts层就可以通过如下代码创建对象,并调用成员方法,以及通过类调用静态方法

import {Shape} from 'libsharedlibrarytest.so'
let shape1 = new Shape(50,50)
let area = shape1.getArea()
let shape2 = new Shape(60,60)
let isBig = Shape.compareArea(shape1, shape2)

静态方法和成员方法在native层定义时的主要区别是什么?

可以重点关注以下两段代码

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<ttsample::Shape> shape = std::make_unique<ttsample::Shape>(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()

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

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

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

对象生命周期管理

c++对象生命周期管理

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


std::unique_ptr<ttsample::Shape> shape = std::make_unique<ttsample::Shape>(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。

  • 打开和关闭 scope 的方法为 napi_open_handle_scope 和 napi_close_handle_scope;

  • N-API 中 scope 的层次结构是一个嵌套的层次结构,任何时候只有一个存活的 scope,所有新创建的 handle 都将在该 scope 处于存活状态时与之关联;

  • scope 必须按打开的相反顺序关闭,在 native 方法中创建的所有 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(线程安全函数)

  • 这种方式通过 napi提供的 napi_create_threadsafe_function API 实现。它允许你将 JavaScript 函数封装成一个线程安全的对象,在其他线程中调用该对象执行 JavaScript 代码。这种方法适用于需要在后台线程中处理长时间运行的任务,而不阻塞 Node.js 事件循环的情况。线程的创建需要使用c/c++中相关的线程创建api进行创建,注意napi_create_threadsafe_function方法的调用一定是在js线程中,不能在c/c++创建的线程中调用,由napi_create_threadsafe_function创建的线程安全对象,可以在其他任何线程中调用。以实现与js线程通信的目的。

AsyncWorker(异步工作者)

  • AsyncWorker 是 N-API 提供的另一种机制,用于在后台线程中执行任务,并在完成时通知主线程。通过napi_create_async_work来创建aysnc_worker。这种方式适用于需要执行较长时间的计算或者 IO 操作,而不影响 Node.js 的主线程响应其他事件的需求。

例子:

#include <node_api.h>
#include <stdio.h>
#include <unistd.h> // 用于 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)

解释说明:

  1. 异步数据结构体和任务函数

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

  3. JavaScript 调用的异步函数

  4. 模块初始化和导出函数

线程安全

  • 如果应用需要进行大量的计算或者 IO 操作,使用并发机制可以充分利用多核 CPU 的优势,提高应用的处理效率。例如,图像处理、视频编码、数据分析等应用可以使用并发机制来提高处理速度。

  • 在多线程环境下,可以使用 napi_create_threadsafe_function 函数创建一个线程安全函数,然后在任意线程中调用。 需要注意:napi_create_threadsafe_function创建需要在js线程,napi_create_threadsafe_function创建的线程安全函数的执行可以在任意线程,napi_create_threadsafe_function创建的线程安全函数的执行是非block的。如果需要等待napi_create_threadsafe_function创建线程安全函数的执行结果,需要单独去写wait的notify的逻辑。

线程函数使用注意事项

  • 在多线程环境下,需要避免使用共享的数据结构和全局变量,以免竞争和冲突。同时,需要确保线程之间的同步和互斥,以避免数据不一致的情况发生。除此之外,仍需注意: 对线程安全函数的调用是异步进行的,对 JavaScript 回调的调用将被放置在任务队列中; 创建 napi_threadsafe_function 时,可以提供 napi_finalize 回调。当线程安全函数即将被销毁时,将在主线程上调用此 napi_finalize 回调; 在调用 napi_create_threadsafe_function 时给如果定了上下文,则可以从任何调用的线程中通过napi_get_threadafe_function_context获取。

性能相关

跨语言调用开销

接口调用

  • 跨语言调用是指在一个程序中使用多种编程语言编写的代码,并且这些代码可以相互调用和交互,ArkTS 调用 C++ 就是一种跨语言调用的方式。使用 N-API 进行函数调用会引入一定的开销,因为需要进行上下文切换、参数传递、函数调用和返回值处理等,这些过程都涉及到一些性能开销。目前,通过 N-API 接口实现 ArkTS 调用 C++ 的场景大致分为两类:ArkTS 直接调用 C++ 接口、c++回调ArkTS的接口。js引擎已经优化了与本地代码(如 C++)的交互,能够快速地将 JavaScript 函数调用转换为本地代码执行,C++ 到 JavaScript 的上下文切换通常比较复杂,涉及到将控制权从本地代码转移到 JavaScript 解释器,所以会引入较多的性能开销。频繁的语言切换可能会影响业务性能,因此需要开发者合理的设计接口调用频率。

数值转换

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

有如下建议:

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

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

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

写在最后

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。

希望这一份鸿蒙学习文档能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!

如果你是一名有经验的资深Android移动开发、Java开发、前端开发、对鸿蒙感兴趣以及转行人员,可以直接领取这份资料

请点击→纯血版全套鸿蒙HarmonyOS学习文档

鸿蒙(HarmonyOS NEXT)5.0最新学习路线

在这里插入图片描述

路线图适合人群:

IT开发人员:想要拓展职业边界
零基础小白:鸿蒙爱好者,希望从0到1学习,增加一项技能。
技术提升/进阶跳槽:发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术

获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习文档

《鸿蒙 (HarmonyOS)开发入门教学视频》

在这里插入图片描述

《鸿蒙生态应用开发V3.0白皮书》

在这里插入图片描述

《鸿蒙 (OpenHarmony)开发基础到实战手册》

OpenHarmony北向、南向开发环境搭建

在这里插入图片描述

《鸿蒙开发基础》

●ArkTS语言
●安装DevEco Studio
●运用你的第一个ArkTS应用
●ArkUI声明式UI开发
.……
在这里插入图片描述

《鸿蒙开发进阶》

●Stage模型入门
●网络管理
●数据管理
●电话服务
●分布式应用开发
●通知与窗口管理
●多媒体技术
●安全技能
●任务管理
●WebGL
●国际化开发
●应用测试
●DFX面向未来设计
●鸿蒙系统移植和裁剪定制
……
在这里插入图片描述

《鸿蒙进阶实战》

●ArkTS实践
●UIAbility应用
●网络案例
……
在这里插入图片描述

获取以上完整鸿蒙HarmonyOS学习文档,请点击→纯血版全套鸿蒙HarmonyOS学习文档

在这里插入图片描述

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值