查看: 155|回复: 3

鸿蒙React Native DeviceInfo TurboModul

[复制链接]
发表于 3 小时前 | 显示全部楼层 |阅读模式
在鸿蒙原生开发中,读取设备信息通常需要调用系统接口并处理权限申请。而在 React Native 鸿蒙化改造中,通过 TurboModule 封装 `@ohos.deviceInfo`,可以实现一行代码同步读取设备型号、系统版本等关键信息,无需额外权限和配置。本文记录这个最简 TurboModule 的完整实现流程,以及开发过程中容易被忽略的几个坑。

为什么需要 DeviceInfo TurboModule

在电商类 App 中,经常需要感知用户设备类型。例如华为折叠屏需要做展开态适配,部分设备的相机效果需要特殊处理,不同系统版本上要启用或禁用特定功能。最初团队使用硬编码判断,但发现不同设备返回值的差异较大。后来通过文档确认,`@ohos.deviceInfo` 已经提供了完整的设备信息字段,于是决定封装成 TurboModule,供 JS 侧统一调用。

`@ohos.deviceInfo` 的 API 设计非常简单,属于 `@kit.BasicServicesKit`,从 API 6 就已提供。它是一个常量表,读取即返回,不需要等待、不需要回调、不需要权限申请。导入方式如下:
  1. import { deviceInfo } from '@kit.BasicServicesKit';
复制代码

使用同样直接:
  1. deviceInfo.deviceType
  2. deviceInfo.marketName
  3. deviceInfo.osFullName
复制代码

与 Scan 的 `startScanForResult` 或 ImagePicker 的异步选择不同,DeviceInfo 是纯同步的静态数据读取,零副作用。正因如此,封装它的 TurboModule 是所有 TurboModule 中最精简的。

能读取哪些设备字段

`deviceInfo` 主要提供三类信息:

设备标识类:
- `deviceType`:设备类型,如 phone、pad、tv、wearable 等。
- `manufacture`:制造商,例如 HUAWEI。
- `brand`:品牌,同样返回 HUAWEI。
- `marketName`:市场型号,如 HUAWEI Mate 60 Pro,适合直接展示。
- `productModel`:认证型号,如 ALN-AL00,做机型判断最可靠。
- `productModelAlias`:API 14+ 新增的型号别名。

系统版本类:
- `osFullName`:完整版本串,如 OpenHarmony-5.0.0.1(Canary1)。
- `displayVersion`:显示版本,可能带特殊标识(如 DEM 表示门店演示样机)。
- `majorVersion`、`seniorVersion`、`featureVersion`、`buildVersion`:版本号分段。

底层信息:
- `abiList`:CPU 架构,如 arm64-v8a。
- `securityPatchTag`:安全补丁日期。
- `bootloaderVersion`:Bootloader 版本。

实际做兼容性判断时,`marketName` 和 `productModel` 最常用。例如判断是否为 Mate 60 Pro:
  1. const isMate60Pro = deviceInfo.marketName.includes('Mate 60 Pro');
复制代码

或者按认证型号前缀判断:
  1. const isALN = deviceInfo.productModel.startsWith('ALN');
复制代码

TurboModule 完整实现

整个模块由四部分构成:JS 接口声明、C++ 头文件、C++ 桥接实现、ArkTS 实现,最后注册到 Package。

JS 侧声明(NativeDeviceInfoModule.ts)
  1. import type { TurboModule } from 'react-native';
  2. import { TurboModuleRegistry } from 'react-native';
  3. export interface DeviceInfo {
  4.   deviceType: string;
  5.   manufacture: string;
  6.   brand: string;
  7.   marketName: string;
  8.   productModel: string;
  9.   displayVersion: string;
  10.   osFullName: string;
  11.   majorVersion: number;
  12.   seniorVersion: number;
  13.   featureVersion: number;
  14.   buildVersion: number;
  15.   abiList: string;
  16.   securityPatchTag: string;
  17.   bootloaderVersion: string;
  18. }
  19. export interface Spec extends TurboModule {
  20.   getDeviceInfo(): Promise<DeviceInfo>;
  21. }
  22. export default TurboModuleRegistry.getEnforcing<Spec>('DeviceInfoModule');
复制代码

注意 `majorVersion` 等字段必须声明为 `number`,因为 ArkTS 侧 `deviceInfo.majorVersion` 返回的就是 number,声明成 string 会导致运行时类型不匹配。

C++ 头文件和桥接
  1. // DeviceInfoModule.h
  2. #pragma once
  3. #include "RNOH/ArkTSTurboModule.h"
  4. namespace rnoh {
  5. class JSI_EXPORT DeviceInfoModule : public ArkTSTurboModule {
  6. public:
  7.   DeviceInfoModule(const ArkTSTurboModule::Context ctx, const std::string name);
  8. };
  9. } // namespace rnoh
复制代码
  1. // DeviceInfoModule.cpp
  2. #include "DeviceInfoModule.h"
  3. namespace rnoh {
  4. DeviceInfoModule::DeviceInfoModule(const ArkTSTurboModule::Context ctx, const std::string name)
  5.     : ArkTSTurboModule(ctx, name) {
  6.   methodMap_ = {
  7.       ARK_ASYNC_METHOD_METADATA(getDeviceInfo, 0),
  8.   };
  9. }
  10. } // namespace rnoh
复制代码

方法参数个数为 0,调用即返回。这是所有只读查询类模块的通用模式。

ArkTS 实现
  1. import { deviceInfo } from '@kit.BasicServicesKit';
  2. import { UITurboModule, UITurboModuleContext } from '@rnoh/react-native-openharmony';
  3. const TAG = '[DeviceInfoModule]';
  4. export class DeviceInfoModule extends UITurboModule {
  5.   static readonly NAME = 'DeviceInfoModule';
  6.   constructor(ctx: UITurboModuleContext) {
  7.     super(ctx);
  8.   }
  9.   async getDeviceInfo(): Promise<Record<string, Object>> {
  10.     try {
  11.       const info: Record<string, Object> = {
  12.         deviceType: deviceInfo.deviceType,
  13.         manufacture: deviceInfo.manufacture,
  14.         brand: deviceInfo.brand,
  15.         marketName: deviceInfo.marketName,
  16.         productModel: deviceInfo.productModel,
  17.         displayVersion: deviceInfo.displayVersion,
  18.         osFullName: deviceInfo.osFullName,
  19.         majorVersion: deviceInfo.majorVersion,
  20.         seniorVersion: deviceInfo.seniorVersion,
  21.         featureVersion: deviceInfo.featureVersion,
  22.         buildVersion: deviceInfo.buildVersion,
  23.         abiList: deviceInfo.abiList,
  24.         securityPatchTag: deviceInfo.securityPatchTag,
  25.         bootloaderVersion: deviceInfo.bootloaderVersion,
  26.       };
  27.       return info;
  28.     } catch (err) {
  29.       console.error(`${TAG} getDeviceInfo failed: ${err}`);
  30.       return {};
  31.     }
  32.   }
  33. }
复制代码

这里返回类型用 `Record<string, Object>`,而不是 `Record<string, string>`,是为了兼容 `majorVersion` 等 number 类型字段。ArkTS 不允许对象字面量直接作为返回类型,必须通过 Record 做泛约束。

Package 注册
  1. // DeviceInfoPackage.h
  2. #pragma once
  3. #include "RNOH/Package.h"
  4. #include "turbomodule/DeviceInfoModule.h"
  5. namespace rnoh {
  6. class DeviceInfoPackageTurboModuleFactoryDelegate : public TurboModuleFactoryDelegate {
  7. public:
  8.   SharedTurboModule createTurboModule(Context ctx, const std::string &name) const override {
  9.     if (name == "DeviceInfoModule") {
  10.       return std::make_shared<DeviceInfoModule>(ctx, name);
  11.     }
  12.     return nullptr;
  13.   }
  14. };
  15. class DeviceInfoPackage : public Package {
  16. public:
  17.   DeviceInfoPackage(Package::Context ctx) : Package(ctx) {}
  18.   std::unique_ptr<TurboModuleFactoryDelegate> createTurboModuleFactoryDelegate() override {
  19.     return std::make_unique<DeviceInfoPackageTurboModuleFactoryDelegate>();
  20.   }
  21. };
  22. } // namespace rnoh
复制代码

Package 代码与既有模块几乎一致,改类名即可。完整注册流程共 7 步:

1. 创建 `NativeDeviceInfoModule.ts` 声明 JS 接口。
2. 创建 C++ 头文件 `DeviceInfoModule.h`。
3. 创建 C++ 桥接 `DeviceInfoModule.cpp` 并注册 `methodMap_`。
4. 创建 `DeviceInfoPackage.h` 定义 Package。
5. 在 PackageProvider 中注册 Package(`#include` + `push_back`)。
6. 在 CMakeLists.txt 中添加 .cpp 路径。
7. 在 ArkTS 侧 `GeneratedPackage.ets` 中进行 import 和 map。

任何一步遗漏,运行时都会在 `TurboModuleRegistry.getEnforcing` 处报找不到模块。

踩坑记录:五个典型问题

坑 1:把同步 API 当异步用

`deviceInfo.xxx` 是同步常量,不需要 await。由于 TurboModule 方法签名要求返回 Promise,写异步方法本身没问题,但内部取值直接同步即可。若写成:
  1. const info = await deviceInfo.majorVersion;
复制代码

在 ArkTS 中是合法但无意义的行为。更严重的是,`await` 一个 number 类型会让结果变成 undefined。定位这类问题时,建议先检查是否所有 `deviceInfo` 字段都被直接赋值,而非带 await。

坑 2:serial 字段不可用

`deviceInfo.serial`(序列号)需要 `ohos.permission.sec.ACCESS_UDID` 权限,且该权限仅开放给系统应用和企业定制应用,普通第三方应用无法获取。如果业务需要设备唯一标识,可考虑 `@ohos.identifier` 中的 `odid`,或自行生成 UUID 做持久化,不要在接口中暴露无意义的 serial 字段。

坑 3:deviceInfo 是编译时常量

所有字段在应用启动时即确定,运行期间不会变化。因此每次调用 `getDeviceInfo()` 都重新读取属于重复工作,虽然影响不大,但可以在 ArkTS 侧做缓存:
  1. private cachedInfo: Record<string, Object> | null = null;
  2. async getDeviceInfo(): Promise<Record<string, Object>> {
  3.   if (this.cachedInfo) {
  4.     return this.cachedInfo;
  5.   }
  6.   // 构造 info ...
  7.   this.cachedInfo = info;
  8.   return info;
  9. }
复制代码

这样能减少一次 JS ↔ C++ ↔ ArkTS 的序列化传输开销。不过对于 Demo 应用,是否缓存对性能影响可以忽略。

坑 4:类型声明不一致

`majorVersion`、`seniorVersion` 等在 ArkTS 中是 number,JS 侧接口若声明为 string,TypeScript 编译不会报错,但运行时条件判断可能出错。例如 `info.majorVersion > 4` 会因 JS 宽松比较正常工作,但用 `===` 则不会做类型转换。最稳妥的方式是从一开始就保持两侧类型完全一致。

坑 5:构造对象时少写字段

`Record<string, Object>` 是非严格类型约束,少一两个字段编译器不会提示。第一版实现时曾漏掉 `buildVersion`,排查许久才发现是构造对象时遗漏。建议养成习惯:写完构造对象后数一数接口字段数,两边对得上才提交代码。

额外注意:deviceType 返回字符串而非枚举

`deviceInfo.deviceType` 返回的是字符串,不是枚举。虽然 `@kit.BasicServicesKit` 中有 `DeviceTypes` 枚举(如 `DEVICE_TYPE_PHONE`、`DEVICE_TYPE_TABLET`),但 deviceInfo 返回的仍是字符串。判断设备类型需用字符串比较:
  1. if (info.deviceType === 'phone') {
  2.   // 手机
  3. } else if (info.deviceType === 'tablet') {
  4.   // 平板
  5. }
复制代码

不同系统版本上返回值可能有细微差异(模拟器上是 phone,平板是 tablet,2in1 设备上是 2in1)。建议封装一个映射函数:
  1. function isTablet(info: DeviceInfo): boolean {
  2.   return info.deviceType === 'tablet' || info.deviceType === 'pad' || info.deviceType === '2in1';
  3. }
  4. function isPhone(info: DeviceInfo): boolean {
  5.   return info.deviceType === 'phone';
  6. }
复制代码

CMake 缓存导致的编译问题

另一个与代码无关但更折磨人的问题是 CMake 缓存。当新增 C++ 文件并修改 CMakeLists.txt 后,如果直接 Build,常常会报 `unknown type name` 或 `Permission denied` 错误。这是因为 ninja 构建系统缓存了旧的编译产物,新文件加入后缓存的依赖图未更新。

解决方案:关闭 DevEco Studio,删除 `harmony/entry/.cxx` 和 `harmony/entry/build` 两个目录,重新打开 IDE 并执行 Rebuild。此后只要修改 CMakeLists.txt,就应主动清理 .cxx 目录,避免浪费时间排查不存在的代码问题。

JS 侧调用与性能

JS 侧使用非常简单:
  1. import DeviceInfoModule from '../native/NativeDeviceInfoModule';
  2. const info = await DeviceInfoModule.getDeviceInfo();
  3. console.log(`设备: ${info.marketName} (${info.productModel})`);
  4. console.log(`系统: ${info.osFullName}`);
复制代码

实际测试中,调用到返回通常在 10ms 以内,主要开销是序列化传输和 JSON 编解码,而读数据本身是零等待。可以在 Demo 页面用按钮触发,以 key-value 列表展示返回字段。

与 react-native-device-info 的对比

RN 社区有 `react-native-device-info` 库,功能类似。区别在于:如果只需要基础设备信息(型号、品牌、系统版本),TurboModule 方案更轻量;如果需要 Device ID、MAC 地址等更多敏感信息,则需要考虑该库或直接调用其他 Kit。对大部分电商场景来说,知道设备型号和系统版本就足够了。

关于版本兼容的一个细节

`productModelAlias` 是 API 14+ 新增字段。如果项目最低支持 API 12,在低版本编译时会报错。若要使用,应在初始化时做条件判断,但 ArkTS 的 `arkts-no-obj-literals-as-types` 规则不允许对象字面量动态扩展字段。因此要么提高最低 API 版本,要么在构造 `info` 对象前做好版本分支,或放弃该字段。

另外,`deviceInfo.displayVersion` 中若出现 `DEM` 字样,代表该设备是门店演示样机,这是设计如此而非 bug。

总结

DeviceInfo TurboModule 是掌握 TurboModule 开发流程的理想入门案例:零权限、零配置、14 个字段、1 个方法,实现代码不到 50 行 ArkTS 加 13 行 C++。整个流程跑通后,后续开发 ScanModule、ImagePickerModule 等复杂模块时,只需套用相同框架,重点处理好异步接口和对象字面量的类型约束即可。关键要点归纳如下:

- 同步常量 API 不要 await。
- serial 字段对普通应用不可用,不要加入接口。
- 类型声明必须保持 number/string 一致。
- 构造对象后核对字段数量。
- 修改 CMakeLists.txt 后先清 .cxx 再编译。
- deviceType 是字符串,需要自行做映射。
- 高版本字段需要做 API 版本兼容处理。
回复

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: 鸿蒙React Native DeviceInfo TurboModul

感谢楼主分享,这篇很实用。最近正好在做鸿蒙化的 RN 改造,DeviceInfo 这块确实是个容易忽略的基础点。 有个问题想请教:文中提到 `majorVersion` 等字段在 ArkTS 侧返回 number,但 JS 侧如果直接用作条件判断,比如 `majorVersion >= 5`,有没有遇到过类型转换或兼容性上的坑?另外 `productModelAlias` 在 API 14+ 才提供,做低版本兼容时是不是需要单独判断一下 API 等级? 期待楼主补全后半部分实现,尤其是 C++ 桥接那块的后续内容。
回复 支持 反对

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: 鸿蒙React Native DeviceInfo TurboModul

这个封装思路很实用,尤其对于电商类App做机型适配来说,把设备信息统一收敛到TurboModule里确实能省掉不少重复的硬编码。`marketName`和`productModel`搭配使用做判断也比较稳妥,一个偏展示,一个偏逻辑校验。 想请教一下,C++桥接部分截断了,后面`methodMap_`里同步返回`Promise`的写法方便补充一下吗?另外在鸿蒙上`getEnforcing`拿不到模块时的报错信息是否友好,有没有遇到过需要降级处理的情况?
回复 支持 反对

使用道具 举报

发表于 3 小时前 | 显示全部楼层

Re: 鸿蒙React Native DeviceInfo TurboModul

感谢分享,正好团队在搞鸿蒙化适配,这个很有参考价值。有个小疑问:DeviceInfo本身是同步常量,但JS侧声明成了`getDeviceInfo(): Promise`,这样走TurboModule异步调用,是RNOH的规范必须这样,还是说实际调用时仍然会同步返回?另外C++那段`methodMap_`好像被截断了,后面是注册了`getDeviceInfo`的同步/异步处理吗?如果方便的话希望补充一下。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-8-6 18:17 , Processed in 0.023592 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部