在鸿蒙原生开发中,读取设备信息通常需要调用系统接口并处理权限申请。而在 React Native 鸿蒙化改造中,通过 TurboModule 封装 `@ohos.deviceInfo`,可以实现一行代码同步读取设备型号、系统版本等关键信息,无需额外权限和配置。本文记录这个最简 TurboModule 的完整实现流程,以及开发过程中容易被忽略的几个坑。
为什么需要 DeviceInfo TurboModule
在电商类 App 中,经常需要感知用户设备类型。例如华为折叠屏需要做展开态适配,部分设备的相机效果需要特殊处理,不同系统版本上要启用或禁用特定功能。最初团队使用硬编码判断,但发现不同设备返回值的差异较大。后来通过文档确认,`@ohos.deviceInfo` 已经提供了完整的设备信息字段,于是决定封装成 TurboModule,供 JS 侧统一调用。
`@ohos.deviceInfo` 的 API 设计非常简单,属于 `@kit.BasicServicesKit`,从 API 6 就已提供。它是一个常量表,读取即返回,不需要等待、不需要回调、不需要权限申请。导入方式如下:
- import { deviceInfo } from '@kit.BasicServicesKit';
复制代码
使用同样直接:
- deviceInfo.deviceType
- deviceInfo.marketName
- 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:
- const isMate60Pro = deviceInfo.marketName.includes('Mate 60 Pro');
复制代码
或者按认证型号前缀判断:
- const isALN = deviceInfo.productModel.startsWith('ALN');
复制代码
TurboModule 完整实现
整个模块由四部分构成:JS 接口声明、C++ 头文件、C++ 桥接实现、ArkTS 实现,最后注册到 Package。
JS 侧声明(NativeDeviceInfoModule.ts)
- import type { TurboModule } from 'react-native';
- import { TurboModuleRegistry } from 'react-native';
- export interface DeviceInfo {
- deviceType: string;
- manufacture: string;
- brand: string;
- marketName: string;
- productModel: string;
- displayVersion: string;
- osFullName: string;
- majorVersion: number;
- seniorVersion: number;
- featureVersion: number;
- buildVersion: number;
- abiList: string;
- securityPatchTag: string;
- bootloaderVersion: string;
- }
- export interface Spec extends TurboModule {
- getDeviceInfo(): Promise<DeviceInfo>;
- }
- export default TurboModuleRegistry.getEnforcing<Spec>('DeviceInfoModule');
复制代码
注意 `majorVersion` 等字段必须声明为 `number`,因为 ArkTS 侧 `deviceInfo.majorVersion` 返回的就是 number,声明成 string 会导致运行时类型不匹配。
C++ 头文件和桥接
- // DeviceInfoModule.h
- #pragma once
- #include "RNOH/ArkTSTurboModule.h"
- namespace rnoh {
- class JSI_EXPORT DeviceInfoModule : public ArkTSTurboModule {
- public:
- DeviceInfoModule(const ArkTSTurboModule::Context ctx, const std::string name);
- };
- } // namespace rnoh
复制代码- // DeviceInfoModule.cpp
- #include "DeviceInfoModule.h"
- namespace rnoh {
- DeviceInfoModule::DeviceInfoModule(const ArkTSTurboModule::Context ctx, const std::string name)
- : ArkTSTurboModule(ctx, name) {
- methodMap_ = {
- ARK_ASYNC_METHOD_METADATA(getDeviceInfo, 0),
- };
- }
- } // namespace rnoh
复制代码
方法参数个数为 0,调用即返回。这是所有只读查询类模块的通用模式。
ArkTS 实现
- import { deviceInfo } from '@kit.BasicServicesKit';
- import { UITurboModule, UITurboModuleContext } from '@rnoh/react-native-openharmony';
- const TAG = '[DeviceInfoModule]';
- export class DeviceInfoModule extends UITurboModule {
- static readonly NAME = 'DeviceInfoModule';
- constructor(ctx: UITurboModuleContext) {
- super(ctx);
- }
- async getDeviceInfo(): Promise<Record<string, Object>> {
- try {
- const info: Record<string, Object> = {
- deviceType: deviceInfo.deviceType,
- manufacture: deviceInfo.manufacture,
- brand: deviceInfo.brand,
- marketName: deviceInfo.marketName,
- productModel: deviceInfo.productModel,
- displayVersion: deviceInfo.displayVersion,
- osFullName: deviceInfo.osFullName,
- majorVersion: deviceInfo.majorVersion,
- seniorVersion: deviceInfo.seniorVersion,
- featureVersion: deviceInfo.featureVersion,
- buildVersion: deviceInfo.buildVersion,
- abiList: deviceInfo.abiList,
- securityPatchTag: deviceInfo.securityPatchTag,
- bootloaderVersion: deviceInfo.bootloaderVersion,
- };
- return info;
- } catch (err) {
- console.error(`${TAG} getDeviceInfo failed: ${err}`);
- return {};
- }
- }
- }
复制代码
这里返回类型用 `Record<string, Object>`,而不是 `Record<string, string>`,是为了兼容 `majorVersion` 等 number 类型字段。ArkTS 不允许对象字面量直接作为返回类型,必须通过 Record 做泛约束。
Package 注册
- // DeviceInfoPackage.h
- #pragma once
- #include "RNOH/Package.h"
- #include "turbomodule/DeviceInfoModule.h"
- namespace rnoh {
- class DeviceInfoPackageTurboModuleFactoryDelegate : public TurboModuleFactoryDelegate {
- public:
- SharedTurboModule createTurboModule(Context ctx, const std::string &name) const override {
- if (name == "DeviceInfoModule") {
- return std::make_shared<DeviceInfoModule>(ctx, name);
- }
- return nullptr;
- }
- };
- class DeviceInfoPackage : public Package {
- public:
- DeviceInfoPackage(Package::Context ctx) : Package(ctx) {}
- std::unique_ptr<TurboModuleFactoryDelegate> createTurboModuleFactoryDelegate() override {
- return std::make_unique<DeviceInfoPackageTurboModuleFactoryDelegate>();
- }
- };
- } // 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,写异步方法本身没问题,但内部取值直接同步即可。若写成:
- 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 侧做缓存:
- private cachedInfo: Record<string, Object> | null = null;
- async getDeviceInfo(): Promise<Record<string, Object>> {
- if (this.cachedInfo) {
- return this.cachedInfo;
- }
- // 构造 info ...
- this.cachedInfo = info;
- return info;
- }
复制代码
这样能减少一次 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 返回的仍是字符串。判断设备类型需用字符串比较:
- if (info.deviceType === 'phone') {
- // 手机
- } else if (info.deviceType === 'tablet') {
- // 平板
- }
复制代码
不同系统版本上返回值可能有细微差异(模拟器上是 phone,平板是 tablet,2in1 设备上是 2in1)。建议封装一个映射函数:
- function isTablet(info: DeviceInfo): boolean {
- return info.deviceType === 'tablet' || info.deviceType === 'pad' || info.deviceType === '2in1';
- }
- function isPhone(info: DeviceInfo): boolean {
- return info.deviceType === 'phone';
- }
复制代码
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 侧使用非常简单:
- import DeviceInfoModule from '../native/NativeDeviceInfoModule';
- const info = await DeviceInfoModule.getDeviceInfo();
- console.log(`设备: ${info.marketName} (${info.productModel})`);
- 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 版本兼容处理。 |