从零开始开发纯血鸿蒙应用
〇、前言
一如上图所示,鸿蒙系统是支持原生开发的,即直接在项目工程里面集成C/C++代码,在术语上,将此类使用C/C++代码实现功能的模块,称之为NDK(Native Development Kit 模块。
在官方指南中,就是用上图选中的模板去创建 NDK 模块的,然而,通常在开发鸿蒙应用的时候,并不会将页面实现放置在 NDK 模块中,集成 NDK 模块也只是为了使用一些更适合用 C/C++ 代码实现的方法,因此,我更建议大家选择 Share Library 或 Static Library,这两种项目模板去创建 NDK 模块:
令一个 hsp 模块或 har 模块,转变成支持 C/C++ 代码的 NDK 模块,很简单,只需在如上图的窗口界面上,将 Enable native
开关打开即可。
一、解耦良器——Adapter
以 Static Library 为例,创建模块并等模板代码导入完成后,可以将该模块的 ets 目录下的有关 pages 的代码删掉,新建一个 adapters 目录,一如下图:
并在 adapters 目录下,新建一个 NAPIAdapter.ets 文件,用于向 ArkTS 层导出相关 NAPI。
NAPI,也即 Node-API,是鸿蒙系统实现 NDK 能力的核心,是每一个想掌握鸿蒙原生开发能力的开发者,所必须了解和掌握的。
在 DevEco Studio 导入的模板代码中,有一个已经实现好的 add 方法,因此,我们可以在 NAPIAdapter.ets 文件中直接调用它:
由于是为了向 ArkTS 层暴露 NAPI,因此 NAPIAdapter 类的构造函数设置私有,只对外提供必要的静态方法。
接着,在 learnNAPI 模块的顶级目录下的 Index.ets 文件中,写上导出语句:export { NAPIAdapter } from './src/main/ets/adapters/NAPIAdapter'
,完成这一步之后,其他普通的 ArkTS 模块,如 entry 模块,想要使用 NAPIAdapter,就只需在模块级别的 oh-package.json5 文件中引入依赖即可:
在实际使用的地方,调用NAPI 就能像调用 ArkTS API 一样:
当然了,少不了要导包 import { NAPIAdapter } from 'learnnapi';
相比官方给出的 NAPI 使用方式:直接在业务功能实现脚本中,用类似 import nativeModule from 'liblearnnapi.so'
,我所使用的方式——增加一个 Adapter 类进行参数和返回值转发,不仅导包语句整齐划一,还有利于实现解耦,即 C/C++ 方法名变更时,只需改一个 Adapter 文件和 index.d.ts 文件,而不用跑到业务代码中一顿找。
二、详学 NAPI
想要较为熟练地使用 NAPI 开发鸿蒙应用,需要掌握以下几方面的能力:
- 如何注册自定义 NAPI
- 如何利用 NAPI 读取 ArkTS 传入的参数,并转换对应的 C/C++ 数据类型
- 如何利用 NAPI 将 C/C++ 数据包装成 ArkTS 可用的返回值
- 如何利用 NAPI 去调用 ArkTS 代码实现的方法
- 如何利用 NAPI 流转自定义的 C++ 对象
1、注册自定义的 NAPI
一个 C/C++ 方法,想要被注册成 NAPI,就需要修改如下两个地方:
1.1、Index.d.ts
在 src/main/cpp/types/lib[module name]目录下,会有一个 Index.d.ts
目录,该目录类似于模块一级目录下的 Index.ets,都是承担对外暴露方法接口的作用,只不过 Index.d.ts 暴露出去的是 NAPI:
Index.d.ts 中声明的方法名,可不是随便取名的,而是必须根据 src/main/cpp/napi_init.cpp 中 Init 方法定义的 napi_property_descriptor 数组去命名。
1.2、napi_property_descriptor 数组
在每个 NDK 模块的 napi_init.cpp 文件中,都会有如下一段大同小异的代码片段:
而其中,最为关键的就是 Init 方法中的 napi_property_descriptor 数组,该数组起到了一种类似路由的作用,将 C/C++ 方法与 Index.d.ts 中声明的接口关联起来,使得鸿蒙系统可以顺利找到对应的C/C++方法体。
napi_property_descriptor 数组的每一个元素,都是相同结构的struct:
实际上,我们可以直接拷贝项目模板生成的代码语句,即 { "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr }
,去修改第一个字段和第三个字段的值,就能完成一个 NAPI 接口的注册。
2、读取参数
因为 ArkTS 与 C/C++ 之间,有着不同的语法规定,因此,ArkTS 传入的参数,是不能直接被 C/C++ 代码读取和使用的,必须利用 NAPI 库中的相关方法去进行数据的转换。
这些数据转换方法,与 C/C++ 语法中规定的基本数据类型是相互一致的,分为字符类型、数字类型和布尔类型,当然了,非基本数据类型的自定义 C++ 类,也是能够利用 NAPI,完成 C/C++ 和 ArkTS 之间的透传。
2.1、读取字符类型数据
通常,读取一个类型为字符类型的参数,需要用如下的代码进行配合:
size_t argc = 3;
napi_value args[3] = {nullptr};
size_t len = 0;
napi_get_cb_info(env, info, &argc, args , nullptr, nullptr);
napi_status status = napi_get_value_string_utf8(env, args[0], nullptr, 0, &len);
if (status != napi_ok) {
return nullptr;
}
char *name = new char[len + 1];
std::memset(name, 0, len + 1);
napi_get_value_string_utf8(env, args[0], name, len + 1, &len);
核心语句是 napi_get_value_string_utf8(env, args[0], name, len + 1, &len);
NAPI 官方参考文档:https://nodejs.org/docs/latest-v12.x/api/n-api.html
2.1、读取数字类型
数字类型主要有整型和双精度浮点型,整型根据数据位宽和符号位进行了进一步的划分。
下面是读取 int32 类型和 double 类型的参数的例子:
int32_t age = 0;
status = napi_get_value_int32(env, args[1], &age);
if (status != napi_ok) {
return nullptr;
}
double height = 0.0;
status = napi_get_value_double(env, args[2], &height);
if (status != napi_ok) {
return nullptr;
}
布尔类型的参数的读取,与数字类型的相似,这里就不再举例。
3、封装返回值
对于返回值,也同样需要借助 NAPI 库方法进行数据转换:
这些以 napi_carete_
为开头的 NAPI 库方法,就是用于封装返回值的方法,从上面可以看出,NAPI 提供了相当丰富的返回值封装方法。
对于数字类型和布尔类型的返回值的封装,由于所使用的 NAPI 库方法的参数规格是相同的,所以,使用方式也不尽相同:
napi_value sum;
napi_create_double(env, value0 + value1, &sum);
return sum;
而字符串类型的返回值,封装代码通常如下:
napi_value result;
std::string json = "a string value";
napi_status status = napi_create_string_utf8(env, json.c_str(), json.length(), &result);
if (status != napi_ok) {
return nullptr;
}
return result;
4、C/C++ 调用 ArkTS 方法
在鸿蒙系统中,不仅能够实现 ArkTS 调用 C/C++ 方法,反过来也是可以的,比如下面这段代码:
核心的 napi_call_function, 其声明原型如下:
5、自定义 C++ 类的透传
C++ 自定义类对象实例,是可以直接透传到 ArkTS 侧的,但是无法在 ArkTS 代码中直接使用,必须重新透传回 C++ 侧才能解析并使用:
三、总结
经过上面的学习,我相信,足以应对大多数鸿蒙原生开发需求,因为数据流转就是 鸿蒙 NDK 开发的重头戏。
坑点记录
如果将 NDK 模块类型定义为 feature,那么每次新增 NAPI 或者改动 C/C++ 代码,就必须将该 NDK 重新部署到手机上,否则就会出现 property not define 或者 not calleable 等运行时错误。