1. 项目背景与Day7的整体回顾
1.1 这个康复训练应用到底在做什么
先把这个项目说清楚。我目前在一块基于 OpenHarmony 的教育开发板(RK3568 芯片)上,用 React Native 开发一个给下肢术后患者做康复训练用的移动端应用。
这块板子其实是当作一个“康复训练终端”来用的:患者跟着屏幕里的动作示范视频做抬腿、踝泵、静蹲这些训练动作,应用负责播视频、计时、计次数,记录每一次训练的结果,最后生成一段时间的趋势曲线,方便康复师远程查看训练量够不够。
选型上,业务侧最早提的需求是“先出一个能在展会上演示的版本”,而且要能跑在国产化终端上。当时手头的硬件只有 OpenHarmony 的开发板,原生开发的话 ArkTS 那套也能写,但团队里另外两个同事都是前端背景,对 React 更熟。我们评估了一下,React Native 在 OpenHarmony 上已经有社区适配,生态里现成的视频播放、图表、手势库都能用,跨端代码以后还能复用到 Android 和 iOS,所以最终定了这个组合。
1.2 Day7 这个时间节点的特殊意义
复盘笔记写在 Day7,是因为这一天刚好把“启动白屏”这颗最硬的钉子拔掉了,整个应用的闭环终于能完整体验。
前六天大致是这样的节奏:
- Day1 到 Day2:搭环境。OpenHarmony SDK、Node、DevEco Studio 全家桶装好,跑通一个 Hello World 级别的 RN 页面。
- Day3 到 Day4:把项目从模板工程迁移到真实业务工程,接入了路由、状态管理,把页面骨架搭起来。
- Day5 到 Day6:集中开发训练流程的主链路,也就是“选动作 → 看视频 → 开始训练 → 提交结果”,同时开始调传感器和本地存储。
到 Day6 晚上,功能逻辑基本写得差不多了,但每次冷启动应用都会卡在首屏白屏两三秒甚至更久,有时候干脆起不来,只能杀掉重进。这个问题不解决,演示的时候基本没法看。Day7 一整天都在处理启动链路,所以这篇笔记的背景和主线,就是围绕“RN 在 OpenHarmony 上从冷启动到首帧渲染”这件事展开的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. React Native 在 OpenHarmony 上的接入路线
2.1 现阶段可行的两条接入路线
先说结论,想要在 OpenHarmony 设备上跑 React Native,目前主流的是两条路线。我不是说只有这两条,但至少在我目前接触到的社区方案和厂商 SDK 里,这两条是可复现、有维护的。
第一条:使用 OpenHarmony SIG 维护的 react-native 适配分支。这个方案的特点是“直接 fork RN 核心库做鸿蒙适配”,开发者写的是标准 RN 组件,底层 JS 引擎通过适配层映射到 HarmonyOS 的 ArkUI 组件。比如你写一个 View 和 Text,适配层会映射到对应的原生容器组件。我们项目用的就是这条路线。
第二条:利用 OpenHarmony 的混合 Web 能力,用 WebView 加载 RN 打的 Web bundle。这个方案实现起来快,但本质上已经不是“原生 RN”了,它把 RN 当成纯前端框架跑在 Web 容器里,性能和原生交互都有明显损耗,尤其是视频播放和传感器读取这类场景,体验会很差。
我个人的判断是,如果你要在 RK3568 这类中低端嵌入式设备上做康复训练这种偏交互的应用,第一条路线才是值得投入的。第二条路线只适合“先证明业务流程”的临时演示。
需要说明一下,这里我做的是基于社区实践的选择。OpenHarmony 的 RN 适配层有几个核心包,包括 ReactNative、ReactNativeCore 这些,需要在 DevEco Studio 里和 npm 工程里各配一遍,两边版本要对齐。我踩过的坑里,有一大半都是版本错位导致的。
2.2 工程配置里最容易翻车的三个地方
具体配置过程中,有三个地方特别容易出问题,这里单独拎出来讲。
npm 包与原生 SDK 版本对齐
RN 的生态里,原生模块和 JS 模块是分开打包的。OpenHarmony 适配的仓库会同时发布 npm 包和 DevEco 工程里的依赖,比如 react-native-harmony 相关包。第一次配置时,我直接用 npm install 拉最新版,结果 DevEco 里编出来的 hap 和 npm 包里的 JS 代码对不上,表现为页面能加载 JS 但原生模块全部找不到,日志里全是 Cannot find native module。
后面我学乖了,直接锁定同一版本号的组合,比如 RN 0.72 对应哪个适配 tag,就全用那个 tag 下的包,不要混合使用大版本。建议写死版本号,不要用 ^ 前缀。
metro 端口与开发板网络
OpenHarmony 开发板在公司内网环境下,经常和电脑不在同一网段,导致 metro 的 bundle 地址访问不到。这其实是在开发机上跑模拟器时不会遇到的问题,真机联调就冒出来了。
解决方法是:确认 Metro 监听地址是 0.0.0.0 而不是 localhost,然后在开发板的 RN 初始化代码里显式配置 bundle 的 URL,用电脑的局域网 IP。比如我这边电脑 IP 是 192.168.1.100,那 bundleUrl 就要写成 http://192.168.1.100:8081/index.bundle?platform=harmony。
原生 so 库与权限声明
OpenHarmony 的 hap 包对系统权限管得比较细,如果没在 module.json5 里声明网络权限,metro 加载永远会失败,而且报错非常隐晦,一开始还以为是白屏问题。需要申请的网络权限包括 ohos.permission.INTERNET,如果是离线加载 bundle,还需要把 bundle 文件打进 rawfile 里。
| 配置项 | 常见问题 | 检查方式 |
|---|---|---|
| npm 与原生包版本 | 版本不对齐导致原生模块找不到 | 统一用相同 tag/版本号 |
| Metro 网络 | bundleUrl 指向 localhost | 改为电脑局域网 IP,确认监听 0.0.0.0 |
| 权限声明 | 缺少 INTERNET 权限 | 检查 module.json5 网络权限配置 |
这三个问题要是第一次接触,每一个都能卡住大半天。我当时就是被这三个问题连环折腾,最后才总结出这套检查顺序。
3. 康复训练应用的功能设计与实现
3.1 功能清单拆解:从动作库到训练报告
把产品需求落到技术实现上,我习惯先画一张功能清单,把“必须做”和“做了更好”分开。康复训练应用的核心场景很固定,不需要像互联网 App 那样堆功能。
我们的“必须做”功能有四块:
- 动作库:展示康复训练动作列表,每个动作有一段示范视频和图文说明。
- 训练执行:选择动作后进入训练页面,播放视频,同时显示计时和组数,患者按节奏完成动作。
- 数据记录:每一次训练结束后,保存训练时长、动作次数、完成时间。
- 趋势报告:用折线图展示最近一周或一个月的训练量变化。
“做了更好”的功能包括账号体系、康复师远程开方、动作自动纠错,这几个我们排到了二期。
动作库的数据结构我用了一个比较简单的 JSON 组织方式,每个动作包含 id、名称、视频地址、建议组数、每组次数、休息时长。视频文件没有走网络加载,而是直接放到 rawfile 里,因为康复训练场景大概率在室内,网络不稳定,离线播放更靠谱。
3.2 用传感器做训练动作计数的实现思路
康复训练里最关键的一个交互是“记录动作次数”。比如踝泵运动,要求患者躺着勾脚尖、绷脚尖,一次完整的动作算一次。让患者自己按按钮计数不现实,所以需要借助设备传感器。
RK3568 开发板上没有特别丰富的传感器,但 OpenHarmony 的 Sensor 接口已经封装了加速度计和陀螺仪。我设计的方案是:训练时,监听加速度计的三轴数据,当检测到“脚部位置的规律性变化”时,判定为完成了一次动作。
具体算法是取加速度计的模长,做低通滤波去噪,然后设置一个阈值。当测量值在短时间内先超过阈值再回落到阈值以下,计为一次。这就类似微信步数的计步原理,只是把阈值和采样频率针对“腿部动作”做了调整。
这块需要注意,传感器事件回调频率很高,如果每一个事件都触发 React 层的 setState,JS 线程会被打爆。我做的优化是:原生侧先把原始采样值缓存起来,每隔一定时间做一次批量过滤判断,只把“计次结果”这一条消息丢给 RN 层,模型上类似“原生侧过滤,JS 层展示”。
typescript复制// 伪代码示意:传感器计次判断
let lastCross = false;
let count = 0;
sensorManager.on('accelerometer', (data) => {
const magnitude = Math.sqrt(data.x * data.x + data.y * data.y + data.z * data.z);
const filtered = lowPassFilter(magnitude);
const overThreshold = filtered > 10.5;
if (overThreshold && !lastCross) {
count += 1;
notifyRN(count); // 只把结果传给 JS 层
}
lastCross = overThreshold;
});
阈值怎么来的?我是拿着开发板实际绑在脚踝上反复做动作,采集了一组数据后取的中间值。没有用特别复杂的模型,因为康复训练本身不追求绝对精准,计次允许存在一两次误差,但一定要稳定。
3.3 训练数据落库与趋势图展示
训练结果我用了轻量级数据库存储。OpenHarmony 自带关系型数据库接口,RN 侧通过原生模块调用的方式写入。最开始我想用 AsyncStorage,但 AsyncStorage 在高频读写大量记录时性能一般,而且对复杂查询支持很弱,所以就放弃了。
表结构很简单:训练记录表,字段包括 id、动作名称、次数、时长、创建时间。查询时按时间排序,取最近 30 条返回给 RN 层绘制折线图。
图表这块我先说结论:RN 生态里的图表库,在 OpenHarmony 适配层里不能保证全部可用。我们试了几个流行库,有的图形渲染直接白屏,有的坐标轴显示错位。最终选了一个逻辑比较轻的 SVG 方案,用 polyline 自己画折线图。康复训练趋势图不需要复杂的交互,只要能看出“练了几天”“每天几次”,所以手写 SVG 完全够用,也避免引入大库造成的兼容问题。
如果你也要做类似的图表功能,我有一个建议:先写一个最小的 demo 页面验证图表库能不能跑,再决定要不要用。不要一上来就按 Web 开发的经验引入重库,最后发现适配层不支持,整个页面跟着崩。
4. 启动白屏问题排查实录
4.1 白屏故障现象与初步定位
白屏是这个项目里最折磨人的问题。现象描述很简单:点击 App 图标后,应用停留在一个全白界面,有时 2 到 3 秒后能进入首页,有时一直白屏,必须杀掉重来。
刚开始我怀疑是 RN bundle 加载慢,因为首屏要执行大量 JS 代码,OpenHarmony 的 JSBundle 解析能力肯定不如手机端。但奇怪的是,同样的 JS 代码,在 Android 模拟器上启动很快,到了开发板上就表现不稳定,有时候快有时候慢,甚至失败。这说明问题不在 JS 层面的逻辑复杂度,而在加载链路。
我做的第一步排查,是把应用跑在 DevEco Studio 里看打印日志。hdc 连接设备后,用命令行过滤关键字:
bash复制hdc shell hilog | grep ReactNative
日志里看到了 Loading JavaScript bundle 相关记录,但没有明显的崩溃堆栈。接着我把时间轴拉长,观察白屏期间 CPU 是否在忙。初步定位到一个关键现象:白屏期间,设备的 SoC 并没有高负载,说明不是 JS 执行阻塞,而是在等某个东西超时——典型的“网络 or 文件读取阻塞”特征。
4.2 从 hilog 到 metro,一步步收敛根因
按照“等待超时”的思路,我把重点放在 bundle 的加载来源上。开发阶段,RN 默认从 Metro Server 加载 bundle。设备端有个配置项指定了 bundleUrl,如果这个地址在设备上访问不通,JS 加载就会一直等,直到超时失败,表现出来就是白屏或偶发启动不了。
我用设备自带的浏览器或 curl 工具去访问电脑的 Metro 地址,发现时通时不通。因为开发板连接的是办公 WiFi,电脑是网线接入,局域网内设备隔离策略会把这部分流量挡掉。更麻烦的是,Metro 默认只绑定 localhost,设备访问不到,所以不是“网络凑巧不通”,而是 Metro 根本没监听在这个网卡上。
把 Metro 改成绑定 0.0.0.0 后,设备能访问到 bundle 了,但启动速度依然不稳定。我继续从 hilog 里挖数据,看到一个 JSBundleURLRequest timed out 的报错。这说明 Metro 虽能访问,但每次打包传输的字节量比较大,加上无线网络信号弱,偶发超时被触发。
这里面还有一个容易忽略的细节:DevEco Studio 的 hap 工程和 npm 工程的 Metro 要同时运行,我第一次排查看漏了一个进程,导致查了很久。
4.3 最终的修复方案与预防手段
解决白屏问题,我最终做了两项改动。
第一项是开发阶段改用“本地 bundle 预加载”。也就是把 Metro 打包生成的 bundle 文件放到 hap 的 rawfile 目录下,App 启动时直接读本地文件,不走网络。这样彻底消除了网络超时这个变量。缺点是每次改 JS 代码,都要重新打包生成 bundle 并重新安装 hap,迭代效率变低。我的处理方式是:日常 UI 细节调整用 Metro 热更新,遇到需要演示或发布时,再打一次本地 bundle 包。
第二项是给首屏加了原生侧的开屏占位图。RN 加载需要时间,这个时间不可避免,那就不要让用户盯着白屏。开屏页用 ArkTS 写一个原生页面,展示 App 名称和 Logo,同时接收 RN 侧的“首帧渲染完成”事件,收到后再关闭开屏页。这样用户感知到的就不再是白屏,而是正常的启动过渡。
另外还做了一层兜底:在原生侧加了超时判断,如果 JS 加载超过 8 秒,自动重新加载一次,避免偶发死等。这层逻辑用 ArkTS 写在原生入口处,和 RN 层无关,任何时候都不会被业务代码影响。
typescript复制// 开屏页简化的处理逻辑
onPageShow() {
const timeout = setTimeout(() => {
if (!this.isRNReady) {
// 超时重试
this.loadJSBundle();
}
}, 8000);
}
onRNFirstFrameRender() {
this.isRNReady = true;
clearTimeout(this.timeout);
this.closeSplash();
}
这套组合拳下来,白屏问题基本解决。真实体验是:从点击图标到进入首页,稳定在 2 到 3 秒,其中大部分时间花在 hap 初始化上,已经没有那种“卡死不知道会不会出来”的感觉了。
5. 多设备适配与性能优化经验
5.1 RK3568 设备树差异对应用层的真实影响
项目过程中,我在不同批次的教学开发板上跑过同一个 hap,发现同样是 RK3568 芯片,不同板卡的屏幕分辨率和触摸屏型号不同,设备树配置也不一样。网上经常有人问“openharmony 的 rk3568 有许多设备树到底咋选”,这个问题直接影响的是系统镜像能不能正确驱动屏幕、触控和传感器。对应用层来说,最直接的感受是:同一套包,在这个板子上页面显示正常,换一块板子字体就变得特别大或特别小,触摸点击位置偏移甚至完全没反应。
我的建议是分两层处理。板级适配层由负责系统的同事去确认设备树和镜像的组合,应用侧只需要做两道防护:第一,布局尽量使用自适应单位,不要写死像素;第二,启动时读取系统屏幕密度,根据密度动态调整字体缩放和触摸热区。
RN 在 OpenHarmony 上对屏幕适配的处理和 Android 类似,px 和 dp 之间的转换由适配层完成,但字体大小有时候会受系统全局缩放影响。我在应用的根组件里加了一个逻辑:如果屏幕密度大于某个阈值,就统一按比例缩小字体基准值,保证小屏板卡上不会被系统放大到离谱。
5.2 性能优化三板斧:图片、列表、重绘
7天开发下来,性能优化我做过的有效手段可以归结为三件事:图片预处理、列表回收、减少无谓重绘。
RK3568 的 GPU 性能不算强,加载大尺寸图片时掉帧特别明显。训练动作的封面图我统一做了裁剪压缩,一张图控制在 80KB 以内,列表滚动就流畅了很多。原始设计稿用的是高清图,一张可能好几 MB,放手机上没问题,放开发板上就是灾难。
列表回收这块,RN 的 FlatList 在适配层上表现还可以,但要注意不要图省事把所有内容都放在一个 ScrollView 里。动作库列表产品上可能就几十个动作,我一开始用的是 ScrollView 加 map,滚动时会有明显迟滞。改成 FlatList 之后,即使数据量增加一倍也没问题。
减少重绘是比较隐蔽的一个优化点。我发现在训练计时页面,每秒更新一次倒计时文本,会用 setState 触发整个页面的渲染。后来我把计时相关的组件单独拆出来,用 memo 包裹,只让计时文本节点自己更新。页面上其他静态内容(比如视频画面、按钮)就不会跟着每秒重绘。
提示:RN 在 OpenHarmony 上的渲染性能和 Android 相比还有差距,尤其动画和频繁 setState 的场景。不要照搬互联网 App 的交互复杂度,能简则简,优先保证训练过程的流畅稳定。
6. 高频问题速查与下一步排期
6.1 7天里高频踩坑速查表
复盘一下这7天遇到的问题,我整理成一份速查表,给后来人做个参考。这些问题基本都是环境和小细节层面的,解决了其实都不难,但没遇到过确实会耗费大量时间。
| 问题现象 | 根因 | 解决方案 |
|---|---|---|
| bundle 一直加载不出来 | Metro 绑定 localhost 或设备不在同一网段 | Metro 监听 0.0.0.0,bundleUrl 用局域网 IP |
| 原生模块找不到 | npm 包与 DevEco 工程依赖版本不一致 | 锁定同一版本组合 |
| 应用启动白屏 | bundle 网络加载超时 | 本地 bundle 预加载 + 开屏占位页 |
| FlatList 滚动卡顿 | 图片过大或数据未做回收 | 封面图压缩 + 使用 FlatList |
| 传感器计次不准 | 原始数据未滤波,阈值不匹配 | 低通滤波 + 实际采集标定阈值 |
| 屏幕字体或点击位置不对 | 设备树不同导致屏幕密度差异 | 读取系统密度动态缩放 |
| 视频播放不稳定 | 高清视频编码格式不支持 | 统一转码为 H.264 + 低码率 |
6.2 接下来的几个重点方向
白屏问题解决后,应用已经可以跑完整个训练闭环。下一步重点有三个方向。
第一个方向是动作识别算法的工程化。目前的计次算法只是最基础的阈值判断,误判率在快速动作时偏高。后面想引入稍微复杂一点的姿态判断,比如利用陀螺仪数据结合角速度变化,区分“有效动作”和“无效抖动”。
第二个方向是训练数据上报。目前数据只存在本机,康复师想看到患者训练情况,得拿着开发板翻记录,这显然不现实。计划在这块 OpenHarmony 设备上接入一个简单的后端接口,把训练记录上报上去,同时支持康复师下发训练计划。
第三个方向是离线视频与在线更新的结合。有些动作要经常更新,如果视频全打进 rawfile 里,更新一次动作库就得重新发版。后续会做“首次内置 + 在线同步”的策略,内置一套基础动作,后续新增的动作通过后台接口下载到本地存储,再替换动作库数据。
按现在的节奏看,再做一周应该能出一个可以小范围试用的版本。到时候我会把完整的训练录制视频和性能数据再整理一篇出来。
