做 React Native for OpenHarmony 也有一段时间了,最近在好几个项目里都绕不开设备信息获取这件事。很多从 Android 转过来的同学会习惯性找 Build.MODEL、Build.SERIAL,但到了鸿蒙的 RN 环境里,这些老方法基本都不可用。老老实实用 DeviceInfo 模块,反而能省掉一堆烦心事。这篇文章我就以 DeviceInfo 为主题,把自己在 RK3568、RK3588 真机上跑通的完整经验整理出来,涉及安装、权限、API 拆解、白屏排查和硬件判断。
初看标题可能觉得这事太窄:设备信息不就 getDeviceId() 拿一下?真到了实际项目里,你会发现问题多得让人头疼。比如拿到一个空字符串,拿到一个不断变化的匿名 ID,或者不同版本的系统返回的字段格式不一样。更别提在开发阶段经常遇到的启动白屏,很多人排查半天,最后发现是 bundle 加载路径配置的问题。这些坑我不希望你再去踩一遍,所以尽量写得具体一点,能直接抄的代码和命令都会给出来。
1. 为什么在鸿蒙的 RN 应用中要单独聊 DeviceInfo
1.1 设备信息模块被很多人低估了
在移动端开发里,设备信息模块最容易被人当成“工具人”:需要上传日志时拿来用一下,出现 bug 问一下机型,然后就没然后了。但实际在 React Native for OpenHarmony 项目里,DeviceInfo 承担的角色比想象中重。
首先是兼容性适配。OpenHarmony 的硬件生态不像主流手机那么统一,既有 RK3568、RK3588 这类开发板,也有商业平板和带屏设备。不同芯片产商对系统 UI、性能调度、相机指令的实现差异很大。产品团队在做灰度策略时,经常需要根据芯片型号、系统版本、设备类型下发不同的页面形态。没有 DeviceInfo 这一层,JS 端想判断“当前是不是平板”“当前跑在哪个芯片上”就只能靠猜。
其次是运营数据上报。做版本升级、崩溃聚合、行为埋点的时候,设备型号是绕不开的维度。崩溃日志里如果只记录包名和报错堆栈,排查起来效率非常低。把 deviceId、deviceModel、systemVersion 一起上报,后端才能快速圈出“某型号设备崩溃率偏高”这样的结论。
最后是安全风控和调试辅助。我在调试真机的时候,经常要快速确认手里的板子当前到底加载了哪套固件。以前的做法是重新编译一个测试包,把 Build.PRODUCT 打印出来,后来直接用 DeviceInfo 在 JS 层取 SoC 型号和系统版本,20 秒就能确认。
1.2 从“能跑”到“跑得稳”:设备信息决定了不少分支逻辑
很多团队的 RN 适配到 OpenHarmony 之后,第一版只是让界面能跑起来,但“能跑”和“跑得稳”之间差着大量分支逻辑。
举个实际例子:OpenHarmony 上某些 API 只在 3.2 Release 以后的版本可用,老版本设备上调用会直接抛异常。如果不在 JS 层读取 getSystemVersion() 做判断,等用户反馈闪退再处理就被动了。还有一种情况是屏幕形态差异。RN 在手机和平板上的布局逻辑经常不一样,DeviceInfo.getDeviceType() 能直接告诉你当前是手机、平板还是桌面设备,省得用 Dimensions.get('window') 去猜。
因为我平时在 RK3568 和 RK3588 两类板子上切换开发,对“同代码不同表现”的感受特别深。RK3568 跑 4K 解码时 CPU 明显更吃力,RK3588 负载则小很多。如果项目里有分辨率渲染的适配逻辑,最好根据 SoC 型号提前分流。这些都是 DeviceInfo 能直接解决的需求。
还有一个容易被忽略的点:原生模块的桥接方式会影响调用时机。在 React Native for OpenHarmony 里,DeviceInfo 通常以 TurboModule 的形式存在,JS 侧初次调用时要完成原生模块的初始化。如果首次调用发生在启动渲染的关键路径上,可能会让首帧变慢。所以不要把所有设备信息读取都堆在页面构造函数里,而是放到异步流程中,避免阻塞首屏。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:搭好 react-native-ohos 开发环境的关键几步
2.1 开发环境到底需要哪些组件
要跑 React Native for OpenHarmony,第一步不是写代码,而是把环境磨顺。我试过在 Windows 和 Ubuntu 上分别搭,建议有条件优先用 Ubuntu 18.04 以上版本,文件路径和编译工具链都会省心一些。整套环境大致包含:
| 组件 | 版本参考 | 主要作用 |
|---|---|---|
| Node.js | 18.x 或 20.x LTS | 运行 npm、Metro、RN CLI |
| DevEco Studio | 4.0 以上 | 编译 OpenHarmony 工程、安装签名、抓日志 |
| OpenHarmony SDK | 4.0 Release 以上 | 提供原生编译所需 SDK |
| hdc 命令行工具 | 随 DevEco Studio 安装 | 连接真机、安装 App |
| React Native for OpenHarmony 脚手架 | 社区适配版本 | 生成 RN 工程与 OpenHarmony 原生壳工程 |
Node.js 的版本别贪新,RN 的工具链通常对某个大版本验证得最充分。我之前用过 Node 21 踩过 Metro 的兼容问题,退回 20 LTS 就安静了。DevEco Studio 安装完成后,要通过 SDK Manager 确认 OpenHarmony SDK 已经下载到本机,否则后面原生工程没法编译。
2.2 创建项目并配置 OpenHarmony 工程的入口
社区脚手架一般会提供一个类似 npx @react-native-ohos/xxx init 的命令。我通常先生成一个标准 RN 工程,再把 OpenHarmony 的原生壳工程放进去。结构看起来大概是:
code复制android/
ios/
ohos/
src/
package.json
index.js
ohos 目录就是 OpenHarmony 的工程目录,里面包含 entry 模块、build-profile.json5、hvigorfile.ts 这些文件。初次构建的时候,DevEco Studio 会提示同步依赖,别跳过。
关键配置在 entry/src/main/module.json5 里。入口 Ability 的 type 需要是 entry,这样才能正常拉起页面。同时要保持 pages 里配置的首页能和 RN 启动的 MainAbility 对应上。如果这里的首页配置不对,最容易出现的现象就是应用安装后点开一直是白屏,这个我们在后面的白屏排查小节会专门展开。
如果你是从已有 Android 项目做 OpenHarmony 移植,不要直接把 android 目录复制过来。OpenHarmony 的原生工程结构、权限声明、Ability 模型都和 Android 差很多。我建议重新生成一个 ohos 壳工程,再把业务 JS 代码和原生 API 调用逐步迁移进去。这样能最大程度减少“移植后跑不起来又不知道问题在哪”的尴尬。
2.3 把真机连接上:RK3568 和 RK3588 的常见差异
真机调试用 hdc 而不是 adb,命令生态很接近,但别混用。连接 RK3568 开发板时,我用 USB 连接后执行:
bash复制hdc list targets
如果看不到设备,先检查开发板的 USB 调试开关,再用 hdc tconn ip:port 走网络连接。RK3588 开发板我遇到的情况是 USB 口供电不稳,经常连接几秒后掉线。换根粗一点的 USB 线,或者使用独立供电的 USB Hub,基本能解决。
这两类开发板在 RN 层跑起来之后,差别主要在性能上。RK3588 的 GPU 更强,复杂列表滚动起来更顺;RK3568 在低电量模式下偶发掉帧。做性能验证的时候,我会明确记录 SoC 型号,不然同样一套代码的耗时数据会互相干扰。设备信息此刻就派上了用场,下一章讲如何接入。
3. DeviceInfo 安装与权限配置:别踩官档的坑
3.1 安装适配鸿蒙的 react-native-device-info 包
在 OpenHarmony 的 RN 工程里,不能直接用 npm 上原生 Android 版本的 react-native-device-info,最好安装社区针对鸿蒙适配过的包。实际操作时,我用的是:
bash复制npm install react-native-device-info --save
如果你之前已经在 package.json 里锁定了版本,确认一下它是否带 ohos 或 openharmony 适配标记。更稳妥的做法是直接去开源仓库的 Release 页面看支持列表,复制它推荐的版本号安装。安装完成后,需要重新构建原生工程,因为 RN 的 TurboModule 要经过原生编译才会注册到应用中。只跑 npm start 重启 Metro 是不够的,很多第一次接入的人会在这里卡一会儿。
装好之后,在 index.js 或入口文件里不需要额外 import,但是使用前请确认原生模块已经链接。React Native for OpenHarmony 目前普遍使用自动链接,如果你看到 Native module cannot be null 之类的报错,检查 ohos 目录里的 oh-package.json5 是否已经加入了对应原生模块的依赖。
3.2 需要在 module.json5 里补的权限和配置
这块是经验里最“隐蔽”的坑。很多 API 在真机上返回空字符串,不是因为调用方式不对,而是原生层被权限挡住了。
在 OpenHarmony 的 Stage 模型下,module 级别的权限声明写在 entry/src/main/module.json5 的 requestPermissions 数组里。比如某些字段实现依赖网络状态,或者需要读取设备标识符,就需要对应的权限。具体要加哪一项,建议先看当前接入的适配包源码,不推荐闭眼全加。
我整理过一个基础配置模板:
json5复制{
module: {
// ...
requestPermissions: [
{
name: "ohos.permission.GET_NETWORK_INFO"
},
// 根据实际适配包需要继续增加
]
}
}
切记改完 module.json5 之后,要重新签名并安装到真机。只刷新 JS 包是感知不到变化的。另外,一些系统字段在未配置对应权限时,返回空值比抛出异常更“安静”,这就是很多人排查半天找不到原因的关键。
权限配置还会带来一个连锁反应:不同固件版本对相同权限的处理策略不完全一致。我在 RK3568 上遇到过一个情况,权限声明写法完全一样,但 3.2 版本固件可以读取网络状态,升级到 4.0 之后返回值就变空了。这类问题很难从 JS 侧发现,因为代码没报错,只是数据缺失。所以建议在设备信息页里把所有字段都原样打出来,固件更新后主动对比一次。
3.3 验证模块是否加载成功
安装配置完之后,不要急着写业务。先在入口组件里跑一个最小验证:
js复制import DeviceInfo from 'react-native-device-info';
function App() {
console.log('getApplicationName:', DeviceInfo.getApplicationName());
console.log('getSystemVersion:', DeviceInfo.getSystemVersion());
console.log('getModel:', DeviceInfo.getModel());
return null;
}
如果控制台能打出正常的包名、系统版本和型号,说明原生模块已经正常工作。如果某个字段是空字符串或 undefined,不要立刻怀疑包有问题,先看真机日志里有没有权限拒绝记录。这里我可以直接给一个结论:在 RK3568 和 RK3588 的官方固件上,getSystemName()、getModel()、getSystemVersion() 通常都能正常返回;getDeviceId() 这类唯一标识接口的返回则跟具体实现关系很大,后面单独说。
4. 核心 API 逐一拆解
4.1 设备标识:getDeviceId 与 getUniqueId
唯一标识是大家最关心的字段,但它也最容易让人困惑。
getDeviceId() 在 Android 上通常映射到 Build.SERIAL 或 Android ID,在鸿蒙适配版本里,这个接口能不能稳定返回,取决于原生层到底用哪个底层字段。我在某些固件上测得的结果是,设备 ID 返回的是类似 MAC 地址格式的字符串,但 MAC 在更高的系统版本上可能会被限制,所以不能把它当成持久不变量。
如果只是想在本地做一个装机用户标识,我更推荐使用 getUniqueId()。它可能基于设备的稳定属性生成,但不同适配版本的策略不同。我的建议是:不要在业务里假定它永远不变,也不要把它上传到后端去关联敏感数据。要做用户维度的匿名 ID,最好在应用安装后自己生成一个 UUID 并存入本地存储,DeviceInfo 的唯一标识只作为辅助参考。
4.2 型号与厂商:getModel、getManufacturer、getBrand
这几个 API 非常直观:
js复制console.log('Model:', DeviceInfo.getModel());
console.log('Manufacturer:', DeviceInfo.getManufacturer());
console.log('Brand:', DeviceInfo.getBrand());
在鸿蒙的开发板上,getModel() 一般返回类似 "RK3568" 的板级型号,getManufacturer() 可能返回芯片厂商或板卡厂商。这里有个细节:不同厂家的 ROM 对 Build.MODEL 定义习惯不同,有的返回商业名称,有的返回硬件平台名。所以展示给用户看的时候,不要直接把原始值丢到 UI 上,最好做成映射表,比如把 "RK3568" 转成“标准开发板”。
我在做设备兼容清单时就踩过这个坑:测试同事报了一个型号名,和产品文档里的名称完全对不上,后来一查是同一批次板子刷了不同厂家的 ROM,getModel() 返回值不同。解决办法是在后端建立一份“型号别名表”,前面再加一层归一化逻辑。
4.3 系统版本:getSystemName、getSystemVersion、getBuildId
系统版本信息用来做功能开关特别有用。getSystemVersion() 返回的是 OpenHarmony 的版本号,比如 "4.0.0" 或 "3.2.0"。我在做权限适配的时候,会在 JS 层做一个版本比较工具:
js复制const systemVersion = DeviceInfo.getSystemVersion();
const versionParts = systemVersion.split('.').map(Number);
function isAtLeast(major, minor = 0, patch = 0) {
return versionParts[0] > major ||
(versionParts[0] === major && versionParts[1] > minor) ||
(versionParts[0] === major && versionParts[1] === minor && versionParts[2] >= patch);
}
getBuildId() 返回的是构建版本,通常包含日期或编译编号,更适合定位具体固件问题。比如用户说“软件版本号一样但问题不一样”,很可能就是 buildId 不同。
4.4 应用自己的信息:getApplicationName 与 getVersion / getBuildNumber
设备信息不只包含硬件,也包含 App 自身的信息。getApplicationName() 返回应用名称,getVersion() 返回版本号,getBuildNumber() 返回构建号。做热更新和版本检查时,这几个接口非常有用。比如:
js复制const appVersion = DeviceInfo.getVersion();
const buildNumber = DeviceInfo.getBuildNumber();
要留意本地打包和 CI 打包的 buildNumber 是否一致。如果不一致,线上反馈版本的指向会歪。我在工程里会把构建号写入原生配置,确保 JS 层拿到的值和应用市场上展示的一致。
4.5 SoC 与硬件信息:getChipset / getSoC / getDeviceType
硬件信息是鸿蒙开发板场景里最能体现价值的一组。getChipset() 或 getSoC() 可以直接返回芯片平台的名称,常见返回值如下:
| API 名称 | 返回值示例 | 适用场景 |
|---|---|---|
| getModel | RK3568 |
UI 展示、故障调查 |
| getSoC / getChipset | Rockchip RK3588 |
性能策略、解码策略 |
| getDeviceType | Handset 或 Tablet |
布局适配 |
| getSystemVersion | 4.0.0 |
能力降级开关 |
我在 RK3568 和 RK3588 之间切换调试时,会在首页顶部显示一个设备信息条,用不到 10 行代码就能在打开 App 的瞬间确认当前固件和芯片。这个方法也帮我解决了一个很常见的痛点:OpenHarmony 的 RK3568 开发板存在多个设备树,选错设备树时系统也能启动,但某些外设工作不正常。此时通过 SoC 型号和系统版本比对当前固件,能快速判断问题是不是出在设备树与固件不匹配上。
5. 两个绕不开的实操问题:启动白屏和设备树选择
5.1 启动白屏的链路排查
“React Native 启动白屏”在 OpenHarmony 上出现的频率比普通 Android 高,很多第一次接触的同学会误以为是自己的业务代码有问题。我排查过几次,结论基本集中在三块。
第一块是 Metro Bundle 没有拉取到。开发模式下,RN 应用启动时需要从 Metro 服务器下载 JS Bundle。真机通过 USB 连接开发机时,如果手机和电脑不在同一个网段,或者 Metro 监听的 IP 没有正确配置,应用就会一直白屏。检查方法很直接:在终端里看 Metro 日志,如果看到 Unable to resolve module 或连接被拒绝,就先处理网络。真机上可以通过 hdc shell "param get const.product.model" 拿到本机 IP,然后在代码里把 jsBundleURL 指向 http://<开发机IP>:8081/index.bundle?platform=ohos&dev=true。
第二块是 离线 Bundle 打包问题。发布版应用一般会关闭 Metro,把 Bundle 打进包里。如果 assets 目录里没有对应的 bundle 文件,或者入口页面启动时加载路径写错,也会白屏。检查 src/main/resources/base/media/ 下有没有 index.bundle,没有就重新跑一次打包命令。
第三块是 原生壳工程的启动页面配置。OpenHarmony 工程里首页 Ability 的配置非常关键,如果默认加载的页面不是 RN 的容器页面,而是某个空白的原生页面,也会白屏。对应关系可以在 module.json5 的 pages 列表里核对。
我见过一个特别容易误导人的情况:开发者在页面里先调用了 DeviceInfo.getDeviceId(),发现返回值有问题,改了一版后又出现白屏,就怀疑是 DeviceInfo 引起的。实际上白屏和 DeviceInfo 无关,纯粹是打包时 assets 目录没生成新的 bundle。所以遇到白屏不要先怀疑第三方模块,按上面三层链路来排除最稳。
5.2 设备信息如何帮你判断 RK3568 的设备树选没选对
很多玩 OpenHarmony 开发板的人都搜过“RK3568 有许多设备树到底咋选”这个问题。设备树选错之后,系统能起来,但 HDMI 输出没画面、Wi-Fi 模块找不到、触摸屏没反应,这些现象都很容易让人误以为是硬件坏了。
通过 DeviceInfo 可以做一个旁路判断。RN 应用跑起来之后,读取 DeviceInfo.getModel() 和 DeviceInfo.getSystemVersion(),再获取一下 DeviceInfo.getBuildId(),把这些值拼到日志里。如果日志显示的板级型号与开发板的实际型号不一致,或者系统版本和烧录固件版本对不上,就要怀疑设备树选择是否正确。
这里要强调一下,RN 的 DeviceInfo 并不能直接读取内核设备树解析结果,它只是读取系统构建时暴露出来的属性。但因为这些属性通常来自设备树和系统编译配置,所以能间接反映当前烧录的固件与设备树匹配度。作为应用层开发者,用这个办法确认现场设备的状态已经足够。
我在公司内部会建议设备组的同事在测试固件上装一个简单的 RN 设备信息页,打开就能看到五六个关键字段,省去大量串串口敲命令的时间。
6. 实战示例:做一个设备信息展示页
6.1 页面布局与数据模型
前面讲了原理和 API,现在给一个可以直接抄的示例。我的目标是做一个设备信息展示页,打开 App 就能看到设备 ID、芯片型号、系统版本等关键字段。
先定义数据模型。由于多个 API 是异步的,我习惯用一个 DeviceInfoSnapshot 对象把它们统一收敛起来:
ts复制type DeviceInfoSnapshot = {
appName: string;
appVersion: string;
appBuildNumber: string;
deviceId: string;
uniqueId: string;
systemName: string;
systemVersion: string;
buildId: string;
model: string;
manufacturer: string;
brand: string;
soc: string;
deviceType: string;
isTablet: boolean;
isEmulator: boolean;
};
然后在页面里维护一个 snapshot 状态,加载中显示 loading,加载完成后渲染一个 ScrollView 里的列表。
6.2 在 RN 生命周期里异步加载设备信息
组件挂载后,用一个 loadDeviceInfo 函数批量读取。要注意部分 React Native for OpenHarmony 的 API 是以异步方式返回的,调用时加上 await。
jsx复制import React, { useEffect, useState } from 'react';
import { View, Text, ScrollView, ActivityIndicator, StyleSheet } from 'react-native';
import DeviceInfo from 'react-native-device-info';
export default function DeviceInfoScreen() {
const [loading, setLoading] = useState(true);
const [snapshot, setSnapshot] = useState(null);
useEffect(() => {
async function load() {
const info = {
appName: await DeviceInfo.getApplicationName(),
appVersion: await DeviceInfo.getVersion(),
appBuildNumber: await DeviceInfo.getBuildNumber(),
deviceId: await DeviceInfo.getDeviceId(),
uniqueId: await DeviceInfo.getUniqueId(),
systemName: await DeviceInfo.getSystemName(),
systemVersion: await DeviceInfo.getSystemVersion(),
buildId: await DeviceInfo.getBuildId(),
model: await DeviceInfo.getModel(),
manufacturer: await DeviceInfo.getManufacturer(),
brand: await DeviceInfo.getBrand(),
soc: await DeviceInfo.getSoC(),
deviceType: await DeviceInfo.getDeviceType(),
isTablet: DeviceInfo.isTablet(),
isEmulator: await DeviceInfo.isEmulator(),
};
setSnapshot(info);
setLoading(false);
}
load();
}, []);
if (loading || !snapshot) {
return <ActivityIndicator style={styles.loading} size="large" />;
}
const rows = [
['应用名称', snapshot.appName],
['应用版本', `${snapshot.appVersion} (${snapshot.appBuildNumber})`],
['deviceId', snapshot.deviceId],
['uniqueId', snapshot.uniqueId],
['系统名称', snapshot.systemName],
['系统版本', snapshot.systemVersion],
['Build ID',
