05 NAPI开发实战演示
1. 简介
本章节,我将以GPIO的读写为例,带各位完整的分析一个NAPI项目应该如何创建,并对工程结构和码源进行详解,后续的使用中,我们将以此为基础,不再对每一个外设与接口都进行详细介绍,只进行必要说明!
我们的应用采用"Native C++"模板,实现了通过NAPI(Node-API)调用Linux内核的命令行接口,在ArkTS上控制Rk3568的GPIO。
本章节虽有一定难度但非常重要!
本章节资料路径
hap包:\05-开发资料\01-OpenHarmory 开发资料\外设测试APP\HAP\GPIO_TEST.hap
工程码源:\05-开发资料\01-OpenHarmory 开发资料\外设测试APP\SRC\GPIO_TEST
2. 实现的目标效果图
输入模式:

输出模式:

3. 代码结构解读
本篇将对核心代码进行解读,软件使用Native C++ 模板生成的工程代码结构如下:

1)napi_init.cpp
这是NAPI开发的核心文件,主要的功能接口函数是在这里被用户定义的,此外NAPI模块注册的初始化代码也生成在此文件下。实现了JavaScript与C++之间的桥接,提供硬件控制功能。
2)CMakeLists.txt
构建配置文件,定义C++模块的编译规则和依赖关系
3)Index.ets
主界面页面,实现代码控制的用户界面,通过NAPI调用底层C++函数。
4)EntryAbility.ets
定义应用的生命周期管理和主窗口创建
5)EntryBackupAbility.ets
实现应用数据的备份和恢复功能
6)module.json5
模块配置文件
7)oh-package.json5
NAPI模块的类型声明包配置文件
4. Index.ets文件调用C/C++函数的流程
4.1 应用框架
整个应用框架可以简单分为三部分:C++侧、eTS侧和各种工具链
- C++侧:包含各种文件的引用、C++或者C代码、Node_API将C++函数与JavaScript关联的信息等。
- eTS侧:包含界面UI、自身方法,调用引用包的方法等。
- 工具链:包含Cmake打包工具在内的系列工具。

4.2 调用、打包流程
在eTS调用C++方法的过程中,调用、打包流程如下:

5. C++侧代码实现
5.1 功能代码编写&解读
下面对C++中几个主要函数进行讲解:
首先是使用2个函数实现GPIO(通用输入输出)的文件操作功能。
void write_gpio_file(const char *filename, const char *value) {
char path[256];
snprintf(path, sizeof(path), "%s/%s", GPIO_PATH, filename);
OH_LOG_Print(LOG_APP, LOG_INFO, GLOBAL_RESMGR, GPIO_TAG,
" %{public}s,%{public}s", GPIO_PATH, filename);
int fd = open(path, O_WRONLY);
if (fd < 0) {
OH_LOG_Print(LOG_APP, LOG_ERROR, GLOBAL_RESMGR, GPIO_TAG,
" open failed for path:%{public}s, errno:%{public}d, error:%{public}s",
path, errno, strerror(errno));
exit(1);
}
if (write(fd, value, strlen(value)) < 0) {
OH_LOG_Print(LOG_APP, LOG_ERROR, GLOBAL_RESMGR, GPIO_TAG,
" open failed for path:%{public}s, errno:%{public}d, error:%{public}s",
path, errno, strerror(errno));
close(fd);
exit(1);
}
close(fd);
}
首先讲解一下write_gpio_file函数,实现的功能是向指定的GPIO文件写入值,用于控制GPIO引脚状态。 实现细节:
1-使用snprintf安全地构建完整文件路径,把路径保存到path下.
2-以只写模式用open打开GPIO文件,成功打开文件后则使用write写入目标值.
提示
发生错误时通过OH_LOG_Print把日志打印出来,方便排查问题。
char* read_gpio_file(const char *filename) {
char path[256];
snprintf(path, sizeof(path), "%s/%s", GPIO_PATH, filename);
OH_LOG_Print(LOG_APP, LOG_INFO, GLOBAL_RESMGR, GPIO_TAG,
" reading %{public}s,%{public}s", GPIO_PATH, filename);
int fd = open(path, O_RDONLY);
if (fd < 0) {
OH_LOG_Print(LOG_APP, LOG_ERROR, GLOBAL_RESMGR, GPIO_TAG,
" open failed for path:%{public}s, errno:%{public}d, error:%{public}s",
path, errno, strerror(errno));
return nullptr;
}
char* buffer = (char*)malloc(32);
ssize_t bytes_read = read(fd, buffer, 31);
if (bytes_read < 0) {
OH_LOG_Print(LOG_APP, LOG_ERROR, GLOBAL_RESMGR, GPIO_TAG,
" read failed for path:%{public}s, errno:%{public}d, error:%{public}s",
path, errno, strerror(errno));
close(fd);
free(buffer);
return nullptr;
}
buffer[bytes_read] = '\0';
// 移除换行符
if (bytes_read > 0 && buffer[bytes_read - 1] == '\n') {
buffer[bytes_read - 1] = '\0';
}
close(fd);
return buffer;
}
再讲解一下write_gpio_file函数,实现的功能是从指定的GPIO文件读取值,用于获取GPIO引脚状态,成功则返回动态分配的内存指针,失败放回nullptr。
实现细节:
1-使用snprintf安全地构建完整文件路径,把路径保存到path下.
2-以只写模式用open打开GPIO文件。
3-使用malloc函数动态分配32字节缓冲区,使用read读取打开的文件的返回的字符串并保存到申请的buffer缓冲区中。
4-将读取到的返回值去除换行符后返回
实现了以上两部分功能以后,就可以实现对文件进行读写进而控制目标外设了,下面再看具体的控制功能函数:
// 设置GPIO方向
static napi_value SetGpioDirection(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value args[1];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
if (argc < 1) {
napi_throw_error(env, nullptr, "Expected 1 argument: direction value (in/out)");
return nullptr;
}
size_t str_size;
napi_get_value_string_utf8(env, args[0], nullptr, 0, &str_size);
char* direction_value = (char*)malloc(str_size + 1);
napi_get_value_string_utf8(env, args[0], direction_value, str_size + 1, &str_size);
write_gpio_file("direction", direction_value);
free(direction_value);
napi_value result;
napi_get_boolean(env, true, &result);
return result;
}
首先是第一个NAPI函数:SetGpioDirection,功能是设置 GPIO 引脚的工作方向(输入模式 "in" 或输出模式 "out") 首先对函数传入的参数进行说明:
env
NAPI环境上下文,用于所以的NAPI调用
info
回调信息,包含JavaScript传入的参数
再分析函数体
1-argc 表示传入的参数个数 args数值用于保存传入的参数值,通过函数napi_get_cb_info可以获取 JavaScript 调用传递的参数信息,比如传入的方向“in"。
2-首先使用函数napi_get_value_string_utf8,不提供缓冲区,只通过调用函数获取传入的字符串长度保存到str_size中。获取到目标字符串的长度后再动态分配内存。再次使用函数使用函数napi_get_value_string_utf8从 JavaScript 参数中提取目标长度的字符串值并转换为 C 字符串并保存到开辟的缓冲区中。
3-调用之前定义的 write_gpio_file 函数向目标文件夹写入方向参数后释放内存。
4-成功写入以后,通过napi_get_boolean创建 JavaScript 布尔值保存到result中并返回。
上一个函数实现了发送指令的功能,我们再来分析一个读取返回值获取IO方向的函数如下
// 读取GPIO方向
static napi_value GetGpioDirection(napi_env env, napi_callback_info info)
{
char* direction_value = read_gpio_file("direction");
if (direction_value == nullptr) {
napi_throw_error(env, nullptr, "Failed to read GPIO direction");
return nullptr;
}
napi_value result;
napi_create_string_utf8(env, direction_value, NAPI_AUTO_LENGTH, &result);
free(direction_value);
return result;
}
函数GetGpioDirection 实现的功能是读取GPIO 引脚的当前工作方向。
参数和返回值和其他NAPI函数都一样,我们只需要关心函数本体:
1-首先调用之前定义的函数read_gpio_file读取指定文件夹对应IO的方向值。
2-再使用函数napi_create_string_utf8将读取到的C字符串转换为JavaScript字符串,并将结果保存到result中进行返回(使用参数NAPI_AUTO_LENGTH可以自动计算字符串长度)
此外,我们还定义了设置GPIO电平值和读取GPIO电平的函数,与上文中同理就不再进行赘述,这里附上代码:
// 设置GPIO电平值
static napi_value SetGpioValue(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value args[1];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
if (argc < 1) {
napi_throw_error(env, nullptr, "Expected 1 argument: value (0/1)");
return nullptr;
}
size_t str_size;
napi_get_value_string_utf8(env, args[0], nullptr, 0, &str_size);
char* value = (char*)malloc(str_size + 1);
napi_get_value_string_utf8(env, args[0], value, str_size + 1, &str_size);
write_gpio_file("value", value);
free(value);
napi_value result;
napi_get_boolean(env, true, &result);
return result;
}
// 读取GPIO电平值
static napi_value GetGpioValue(napi_env env, napi_callback_info info)
{
char* value = read_gpio_file("value");
if (value == nullptr) {
napi_throw_error(env, nullptr, "Failed to read GPIO value");
return nullptr;
}
napi_value result;
napi_create_string_utf8(env, value, NAPI_AUTO_LENGTH, &result);
free(value);
return result;
}
5-2 注册模块 我们拉到napi_init.cpp
函数的末尾,进行编写的功能模块注册。
注册模块写法是固定的,Init函数中我们在napi_property_descriptor desc[]
中需要补充的部分中,把工程中实现的功能函数与需要暴露的接口进行关联即可
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
napi_property_descriptor desc[] = {
{ "setGpioDirection", nullptr, SetGpioDirection, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "getGpioDirection", nullptr, GetGpioDirection, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "setGpioValue", nullptr, SetGpioValue, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "getGpioValue", nullptr, GetGpioValue, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "exportGpio", nullptr, ExportGpio, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "unexportGpio", nullptr, UnexportGpio, nullptr, nullptr, nullptr, napi_default, nullptr }
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
napi_module
用于表述模块信息,一般需要修改的也就是模块名称nm_modname
,然后进行注册即可。
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = ((void*)0),
.reserved = { 0 },
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
napi_module_register(&demoModule);
}
6. 接口代码实现
我们需要在Index.d.ts
文件中对对外提供接口的方法和简单的说明等:
export const setGpioDirection: (direction: string) => boolean
export const getGpioDirection: () => string
export const setGpioValue: (value: string) => boolean
export const getGpioValue: () => string
export const exportGpio: () => boolean
export const unexportGpio: () => boolean
这里的接口名称和注册模块时对外提供的接口名称一致。
": "后面括号内的为需要传入的参数,"=>"为返回值,这里返回的是JavaScript的布尔类型
接下来就是在CMakeLists.txt
中配置CMake打包参数:
# the minimum version of CMake.
cmake_minimum_required(VERSION 3.5.0)
project(GPIO)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
if(DEFINED PACKAGE_FIND_FILE)
include(${PACKAGE_FIND_FILE})
endif()
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include)
add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC
libace_napi.z.so
libhilog_ndk.z.so)
CMakeLists
文件也基本不需要进行修改,一般就是额外添加一个系统库,比如我这里只是在末尾添加了一个libhilog_ndk.z.so
库
7. ets侧代码实现
前面铺垫了这么多,就是为了能够在这里调用相关的功能函数。
我们先在文件开头导入我们定义好的NAPI模块如下:
import testNapi from 'libentry.so'
再定义本示例工程用到的struct组件,其中@State
是响应式状态变量,数据发生变化时,UI界面会自动进行更新,定义了一些本工程中用到到IO方向,电平值和标题等内容。
@Entry
@Component
struct Index {
@State title: string = 'ShiMeta Pi';
@State currentMode: string = 'out'; // 当前GPIO方向模式
@State currentValue: string = '0'; // 当前GPIO电平值
@State message: string = 'GPIO154(IO4_D2)控制';
private intervalId: number = -1; // 定时器ID
我们的Index.ets
部分代码主要分为2部分,功能函数和UI模块,把功能函数进行二次封装的目的是提高代码的可读性,且方便后期进行功能更新,是比较好的编程习惯。下面先对UI中用到的功能函数进行详解。
7.1 功能函数详解
我们为大家选取了以下几个函数进行详解:
// 读取当前GPIO方向
private getCurrentGpioDirection() {
try {
const direction = testNapi.getGpioDirection();
this.currentMode = direction;
hilog.info(DOMAIN, 'GPIO', `当前GPIO154方向: ${direction}`);
} catch (error) {
hilog.error(DOMAIN, 'GPIO', `读取GPIO方向失败: ${error}`);
}
}
函数getCurrentGpioDirection
实现的功能是读取IO的方向,通过调用声明的C++函数getGpioDirection
读取方向值并更新到表示当前IO工作模式的响应式变量中。
// 设置GPIO电平值
private setGpioValue(value: string) {
try {
testNapi.setGpioValue(value);
this.currentValue = value;
this.message = `GPIO154电平已设置为${value === '1' ? '高电平' : '低电平'}`;
hilog.info(DOMAIN, 'GPIO', `GPIO154电平设置为: ${value}`);
} catch (error) {
hilog.error(DOMAIN, 'GPIO', `设置GPIO电平值失败: ${error}`);
}
}
函数setGpioValue
实现的功能是在输出模式下设置IO的输出电平,通过调用声明的C++函数setGpioValue
把设置值写入IO,并更新到表示当前IO输出电平状态的响应式变量中。
// 启动定时器(输入模式下每0.1秒读取电平状态)
private startValuePolling() {
this.stopValuePolling(); // 先停止之前的定时器
this.intervalId = setInterval(() => {
if (this.currentMode === 'in') {
this.getCurrentGpioValue();
}
}, 100);
}
// 停止定时器
private stopValuePolling() {
if (this.intervalId !== -1) {
clearInterval(this.intervalId);
this.intervalId = -1;
}
}
函数startValuePolling
和stopValuePolling
分别用于启停定时器。
其中的函数setInterval
是JavaScript内置函数,实现的功能分别是周期性执行回调函数(这里是100ms),创建成功以后会返回一个定时器ID,ID为-1表示没有定时器或定时器创建失败。
对应的函数clearInterval
也是JavaScript内置函数,实现的功能是清除指定ID的定时器。
// 组件初始化时读取当前状态
aboutToAppear() {
this.getCurrentGpioDirection();
this.getCurrentGpioValue();
// 如果初始模式是输入模式,启动定时器
if (this.currentMode === 'in') {
this.startValuePolling();
}
}
// 组件销毁时清理定时器
aboutToDisappear() {
this.stopValuePolling();
}
函数aboutToAppear
和aboutToDisappear
分别是APP启动的初始化过程和退出时的去初始化过程,调用的函数都是上文中解释过的。
其他功能函数请看码源,相信大家理解了上述函数剩下的理解起来易如反掌,这里就不进行赘述了。
7.2 UI界面函数详解
build() {
Row() {
Column({ space: 40 }) {
Text(this.title)
.fontSize(40)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 30 })
.textAlign(TextAlign.Center)
Text(this.message)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
.textAlign(TextAlign.Center)
// 当前模式显示标签
Text(`当前模式: ${this.currentMode === 'out' ? '输出模式' : '输入模式'}`)
.fontSize(24)
.fontColor(this.currentMode === 'out' ? '#FF6B35' : '#007DFF')
.fontWeight(FontWeight.Medium)
.textAlign(TextAlign.Center)
.padding(20)
.backgroundColor(this.currentMode === 'out' ? '#FFF5F0' : '#F0F8FF')
.borderRadius(10)
.width('100%')
// 当前电平值显示标签
Text(`当前电平: ${this.currentValue === '1' ? '高电平(1)' : '低电平(0)'}`)
.fontSize(20)
.fontColor(this.currentValue === '1' ? '#DC3545' : '#28A745')
.fontWeight(FontWeight.Medium)
.textAlign(TextAlign.Center)
.padding(15)
.backgroundColor(this.currentValue === '1' ? '#FFF0F0' : '#F0FFF0')
.borderRadius(8)
.width('100%')
// 刷新状态按键
Button('刷新当前状态')
.width('60%')
.height(60)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.backgroundColor('#28A745')
.borderRadius(10)
.margin({ bottom: 20 })
.onClick(() => {
this.getCurrentGpioDirection();
// 只在输入模式下读取电平值
if (this.currentMode === 'in') {
this.getCurrentGpioValue();
}
this.message = '状态已刷新';
})
// GPIO方向切换按键
Button(`切换到${this.currentMode === 'out' ? '输入' : '输出'}模式`)
.width('80%')
.height(80)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.backgroundColor(this.currentMode === 'out' ? '#007DFF' : '#FF6B35')
.borderRadius(15)
.onClick(() => {
this.toggleGpioDirection();
})
// GPIO电平值控制按钮组(仅在输出模式下显示)
if (this.currentMode === 'out') {
Row({ space: 20 }) {
Button('设置低电平(0)')
.width('45%')
.height(70)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.backgroundColor('#28A745')
.borderRadius(12)
.onClick(() => {
this.setGpioValue('0');
})
Button('设置高电平(1)')
.width('45%')
.height(70)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.backgroundColor('#DC3545')
.borderRadius(12)
.onClick(() => {
this.setGpioValue('1');
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
// 说明文字
Column({ space: 10 }) {
Text('GPIO154控制说明:')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Text('• 输出模式: GPIO154作为输出引脚,可设置高/低电平')
.fontSize(14)
.fontColor('#666666')
.textAlign(TextAlign.Start)
.width('100%')
Text('• 输入模式: GPIO154作为输入引脚,可读取电平状态')
.fontSize(14)
.fontColor('#666666')
.textAlign(TextAlign.Start)
.width('100%')
Text('• 点击切换按键可在输入/输出模式间切换')
.fontSize(14)
.fontColor('#666666')
.textAlign(TextAlign.Start)
.width('100%')
Text('• 输出模式下可点击按钮设置高电平(1)或低电平(0)')
.fontSize(14)
.fontColor('#666666')
.textAlign(TextAlign.Start)
.width('100%')
Text('• 点击刷新按键可读取当前实际状态')
.fontSize(14)
.fontColor('#666666')
.textAlign(TextAlign.Start)
.width('100%')
}
.width('100%')
.padding(20)
.backgroundColor('#FAFAFA')
.borderRadius(10)
}
.width('100%')
.padding(30)
.justifyContent(FlexAlign.Center)
}
.height('100%')
.backgroundColor('#F5F5F5')
}
}
大家学习了上一章节的"ArkTS入门",上述代码也就一目了然了,就是在水平和竖直方向的线性布局中放入一些按键和文本标签组件,在组件下方添加一些属性比如大小、背景颜色以及点击事件等。
我们在点击事件中使用的功能函数就是我们通过NAPI方式实现的。
8. 代码编译烧写&程序运行
完成上述代码的编写以后,连接设备成功就可以在DevEco Studio中使用一键编译下载运行我们的程序:

等待编译完成后,通过开发板屏幕可以发现会自动进入程序:

接下来,我们点击"切换到输入模式"按键。
不出意外的话应该会闪退回桌面,因为我们还没有在板卡的终端中导出目录和给导出的目录加上可执行权限,我们先通过HDC进入开发板终端,依次输入指令:
echo 154 > /sys/class/gpio/export
chmod 777 /sys/class/gpio/gpio154/*

此时我们可以正常使用程序了。
下面对给大家解释一下原因:
指令echo 154 > /sys/class/gpio/export
表示在gpio目录下导出对应的IO154,此时内核会创建一个属于IO154的文件夹,我们后续就是对这个文件夹进行操作。
指令chmod 777 /sys/class/gpio/gpio154/*
就是给导出的文件gpio154加上权限。chmod
表示权限设置,根据Linux内核下的文件权限表示,其中的7=4+2+1:读(4) + 写(2) + 执行(1) = 完全权限,其中的777: 所有者(7) + 组用户(7) + 其他用户(7) = 所有用户完全权限,末尾的*
是通配符,表示该目录下的所有文件。
而Linux系统默认GPIO文件权限为644,应用程序不是以root用户执行的,所以没有写入权限导致无法执行相应的指令,而OpenHarmony的终端则默认就是root身份运行,所以可以在终端对文件默认权限进行修改。
提示
这里设置权限777是为了方便调试,在一般的生产环境中可能会设置为664防止程序被其他用户篡改
那肯定有朋友问了,为什么不使用指令chmod 777 /sys/class/gpio/*
,这样不就可以在NAPI程序中进行导出操作了,确实是这样的,不过使用指令导出以后的gpio154文件并没有被设置为777权限,因为chmod
指令只会修改已有目录的权限。我们在软件中导出以后,还需要再次回到终端给导出的gpio154加上777权限程序才可以正常执行,为了方便就没有怎么做。我们也在napi_init.cpp
和Index.d.ts
程序中为大家提供了导出IO和取消导出IO的接口,大家可以自行尝试在Index.ets
程序中加入这些接口。
大家可以使用万用表和杜邦线等工具对程序进行测试,笔者已经测试过了但受限于篇幅并考虑到后续会有专门的章节讲解GPIO,我不在这里演示了。
9. 总结
本章节的内容较长,介绍了C++代码如何通过工具链与JavaScript进行关联,以及eTS文件如何调用so包提供的接口,同时通过解读代码带大家学会了C++代码的具体编写和打包流程。是本教程NAPI开发教程中非常重要的一部分,大家可以对照我们的码源进行回顾,有ShiMetaPi M4-R1板卡的也可以按照教程进行复刻。
通过GPIO读写实例,完整展示了NAPI项目的创建和开发流程,涵盖了C++侧代码实现、接口定义、CMake配置、ArkTS界面开发等关键环节,为后续外设接口开发奠定了坚实基础。
10: NAPI开发常见问题
常见问题1:系统命令无法修改权限
开发某个外设时,可能会遇到系统命令无法修改权限的问题,导致无法正常使用。

解决方法:
# 1. 重新挂载根文件系统为可读写
mount -o remount,rw /
# 2. 更改 ifconfig 权限
chmod 777 /bin/ifconfig

常见问题2:ArkTS 不支持隐式声明

ArkTS要求使用明确的类型声明(显式类型),不能使用 any
或 unknown
类型(隐式类型)。
常见问题3:修改应用名称

常见问题4:是不是需要每次开发前都打开终端设置权限?
实际工程中,我们会在系统镜像中通过增加系统启动脚本、udev规则管理权限等方式修改系统文件权限。
常见问题5:应用的NAPI接口在调用时闪退(重要!)
常见的情况是,点击了某个按键,其点击事件中调用了某个NAPI接口,但是应用闪退了。
因为软件默认编译出来的是64位的系统,开发板是32位,如果要在开发板中运行,需要添加一个32位的编译器,如下图:
先在entry
目录下找到文件build-profile.json5
:

"buildOption": {
"externalNativeOptions": {
"path": "./src/main/cpp/CMakeLists.txt",
"arguments": "",
"cppFlags": "",
"abiFilters": [
"arm64-v8a",
"armeabi-v7a"
]
}
}
但是HarmonyOS是不支持32位的,只有OpenHarmony支持板卡的32位处理器,所以会报错。
在工程根目录下找到同名文件build-profile.json5
:

把以上内容修改为:
"products": [
{
"name": "default",
"signingConfig": "default",
"compileSdkVersion": 12,
"compatibleSdkVersion": 12,
"targetSdkVersion": 12,
"runtimeOS": "OpenHarmony",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
]

修改完成以后重新配置一遍工程结构:


最后,如果有问题的话:遇事不决-重启大法!