健康数据同步
健康数据同步是 HealthAide 应用中负责把杰理(Jieli)智能手表/手环通过低功耗蓝牙(BLE)采集的健康数据(心率、血氧、运动、定位、AI 云端消息等)读取到手机端、解析并持久化到本地 Room 数据库的完整链路,涵盖设备侧同步命令、本地数据库访问层与数据对外形态(VO)。
Purpose and Scope
本页讲解 HealthAide(HealthAide_V1.1.0_SDK_V1.14.0)中"健康数据同步"这一能力的端到端实现:
- 设备侧同步命令(JL RCSP SDK 中
SportsInfoStatusSyncCmd等 Sync 命令族)如何被 App 使用; - 同步得到的原始数据如何通过
HealthDataDbHelper单例写入 Room 数据库(HealthDatabase及其 DAO); - 同步数据的实体形态(
HealthEntity、SportRecord、LocationEntity、AICloudMessageEntity、User)与对外数据形态(VO,如血氧日/月聚合); - 控制同步行为的配置开关(
HealthConstant)。
不在本页范围:单类健康指标(如血氧详情)的业务页面、运动轨迹算法、AI 云端对话逻辑分别属于运动记录、血氧数据等兄弟页面;BLE 连接管理与 SDK 初始化细节属于蓝牙连接相关页面。本页只聚焦"数据从设备到本地库再到上层消费"的同步机制本身。
Overview
HealthAide 是一个围绕 JL 系列智能手表/手环的 Android 健康伴侣应用。手表端持续采集用户的健康与运动数据;App 端通过 BLE 连接设备后,使用杰理 RCSP 协议中的同步命令族把设备上的数据拉取到手机,随后在本地完成解析与持久化,供健康看板、历史记录等 UI 消费。
整个同步体系分为三层:
- 命令层:以
SportsInfoStatusSyncCmd为代表的 RCSP 同步命令(固件参数FirmwareStopSportsParam、实时数据响应ReadRealDataResponse等),负责与设备交互; - 持久化层:
HealthDataDbHelper(进程内单例)→HealthDatabase(Room 数据库)→ 各 DAO(HealthDao、SportRecordDao、LocationDao、AICloudMessageDao、UserDao)→ 实体; - 数据形态层:以
BaseVo/BaseParseVo/BaseLiveDataVo为基类的 VO 体系(如BloodOxygenDayVo、BloodOxygenMonthVo),把库表记录转换为 UI 可直接渲染的聚合视图。
HealthConstant 中的静态开关(如 SYNC_DEV_POWER)决定同步内容是否启用;VirtualDataHelper 则为无设备场景提供虚拟数据,用于演示与联调。
Architecture
flowchart TD
subgraph sg_Device["设备侧 (JL Watch)"]
Watch["JL 手表/手环<br/>(BLE + RCSP 协议)"]
end
subgraph sg_Command["同步命令层"]
SyncCmd["SportsInfoStatusSyncCmd 等<br/>Sync 命令族"]
ParseCmd["FirmwareStopSportsParam<br/>ReadRealDataResponse 解析"]
end
subgraph sg_Persist["持久化层"]
Helper["HealthDataDbHelper<br/>(DCL 单例)"]
DB["HealthDatabase<br/>(Room)"]
Dao["HealthDao / SportRecordDao<br/>LocationDao / AICloudMessageDao"]
end
subgraph sg_Entity["实体层"]
Ent["HealthEntity / SportRecord<br/>LocationEntity / AICloudMessageEntity"]
end
subgraph sg_Vo["数据形态层"]
Vo["BaseVo / BaseParseVo / BaseLiveDataVo<br/>BloodOxygenDayVo / BloodOxygenMonthVo"]
end
subgraph sg_UI["上层消费"]
UI["ViewModel / UI 看板 / 历史记录"]
end
Watch -->|"BLE 数据同步"| SyncCmd
SyncCmd --> ParseCmd
ParseCmd --> Helper
Helper --> DB
DB --> Dao
Dao --> Ent
Ent --> Vo
Vo --> UI
架构解读:数据流是单向的"设备 → 命令 → 解析 → 落库 → 出库"。HealthDataDbHelper 是整个持久化层的唯一入口,采用双重检查锁(DCL)单例,确保全进程只构建一次 HealthDatabase,避免重复建库开销;DAO 层屏蔽了 Room 的 SQL 细节,实体层定义表结构,VO 层负责把存储模型转换成按日/月聚合的展示模型。HealthConstant 的开关(如 SYNC_DEV_POWER)在命令层决定同步哪些数据项。
主内容:同步链路实现剖析
1. 设备侧同步命令(JL RCSP SDK)
健康数据的来源是手表固件。App 通过杰理 RCSP 协议中的同步命令族与设备交互,其中典型代表是 com.jieli.jl_rcsp.model.command.watch.SportsInfoStatusSyncCmd。该命令涵盖运动状态上报、固件停止运动参数、实时数据读取等多种子结构。测试代码展示了其用法:
FirmwareStopSportsParam:从固件下发的停止运动参数中解析出fileId等字段;ReadRealDataResponse:解析设备实时数据响应。
这类命令统一继承 RCSP 的命令模型,App 侧通过 WatchManager(WatchManagerByJL / WatchManagerByCustom,见 WatchManagerTest)发送与接收,回调中携带解析后的参数对象。同步内容的开关集中在 HealthConstant:
SYNC_DEV_POWER(默认true)——是否同步设备电量;KEY_ASSERT_RES_SYNC(SharedPreferences key)——是否更新 assert 资源文件(固件资源同步)。
2. 持久化层:HealthDataDbHelper 单例
同步解析出的数据统一通过 HealthDataDbHelper 写入本地库。该类是典型的懒加载双重检查锁(DCL)单例:
public class HealthDataDbHelper {
private final static String TAG = HealthDataDbHelper.class.getSimpleName();
@SuppressLint("StaticFieldLeak")
private volatile static HealthDataDbHelper instance;
private final HealthDatabase mHealthDatabase;
private HealthDataDbHelper() {
Context context = HealthApplication.getAppViewModel().getApplication();
if (null == context) {
throw new NullPointerException("Application is null.");
}
mHealthDatabase = HealthDatabase.buildHealthDb(context);
}
public static HealthDataDbHelper getInstance() {
if (null == instance) {
synchronized (HealthDataDbHelper.class) {
if (null == instance) {
instance = new HealthDataDbHelper();
}
}
}
return instance;
}
...
}
Source: HealthDataDbHelper.java
设计意图:
volatile+ 双重检查保证多线程下仅创建一次实例,同时避免每次获取都加锁的同步开销——同步写入健康数据可能发生在 BLE 回调线程、业务线程等多个线程,必须保证并发安全;- 构造时校验 Application 非空并抛出
NullPointerException,把"依赖注入缺失"这类配置错误尽早暴露,而不是等到首次写库时才失败; @SuppressLint("StaticFieldLeak")是已知取舍:单例持有HealthDatabase(间接持有 Context),换取全进程唯一数据库连接,避免多个连接实例间的写冲突与内存膨胀。
实例对外暴露 5 个访问器,供同步落库与查询两侧使用:
public HealthDao getHealthDao() {
return mHealthDatabase.HealthDao();
}
public SportRecordDao getSportRecordDao() {
return mHealthDatabase.SportRecordDao();
}
public LocationDao getLocationDao() {
return mHealthDatabase.LocationDao();
}
public AICloudMessageDao getAICloudMessageDao() {
return mHealthDatabase.AICloudMessageDao();
}
public HealthDatabase getHealthDatabase() {
return mHealthDatabase;
}
Source: HealthDataDbHelper.java
3. 数据库与 DAO 划分
HealthDatabase(Room 数据库,通过静态工厂 buildHealthDb(context) 构建)按业务域拆分为 5 个 DAO,对应 5 类同步数据:
| DAO | 对应实体 | 同步数据类型 |
|---|---|---|
HealthDao | HealthEntity | 健康指标(心率、血氧等) |
SportRecordDao | SportRecord | 运动记录(含固件 fileId 关联) |
LocationDao | LocationEntity | 运动轨迹 GPS 定位点 |
AICloudMessageDao | AICloudMessageEntity | AI 云端消息(对话/语义结果) |
UserDao | User | 用户信息 |
这种拆分遵循单一职责:不同来源(设备传感器、运动引擎、云端 AI)的数据写入互不干扰,同步任务可以按数据域独立调度;同时每个 DAO 独立承担增删改查,UI 层可按需只观察某一类数据的变化。
注:
HealthDao、HealthDatabase内部 SQL/迁移实现细节不在本次源码读取范围内,本页仅描述其在同步链路中的职责与接口契约。
4. 数据形态层:VO 聚合
data/vo 包提供三层基类与具体 VO:
BaseVo:VO 根基类;BaseParseVo:负责把设备原始字节/命令参数解析为 VO(解析职责下沉到 VO 层,保持 DAO 只做存储);BaseLiveDataVo:与 LiveData 结合的响应式 VO,同步完成即可通知 UI。
血氧模块是典型示例:BloodOxygenBaseVo → BloodOxygenDayVo(按日聚合)/ BloodOxygenMonthVo(按月聚合)。同步命令每收到一段血氧数据,解析后先写入 HealthEntity,再经 DAO 查询聚合出日/月视图,供看板图表直接消费。
5. 虚拟数据:VirtualDataHelper
data/db/VirtualDataHelper 用于无真实设备场景:按 HealthConstant 的开关生成模拟健康数据写入同一套 DAO,使 UI、图表与同步链路在无手表时也能完整跑通(联调/演示/自动化测试)。它复用同一持久化入口,保证"真实同步"与"虚拟同步"对上层完全透明。
Core Flow
sequenceDiagram
participant W as JL 手表 (BLE)
participant B as BleManager / WatchManager
participant P as 命令解析 (SportsInfoStatusSyncCmd)
participant H as HealthDataDbHelper (单例)
participant D as Room: HealthDatabase + DAO
participant V as VO / ViewModel
participant U as UI 看板
W->>B: 上报/响应健康数据 (RCSP)
B->>P: 分发 Sync 命令
P->>P: 解析 FirmwareStopSportsParam / ReadRealDataResponse
P->>H: 写入解析结果 (健康/运动/定位/消息)
H->>D: insert/update 对应 DAO
D-->>H: 行 ID / 结果
H-->>P: 写库完成回调
P->>V: BaseLiveDataVo 通知数据变更
V->>U: 刷新看板/历史记录
D-->>V: 查询聚合 (BloodOxygenDayVo/MonthVo)
流程要点:同步是"拉取-解析-落库-通知"四步。命令解析层(P)与持久化层(H)通过 DAO 接口解耦,使新增一种健康数据类型只需新增命令解析与对应 DAO 写入,无需改动单例与数据库骨架;写库完成后通过 LiveData 形态的 VO 主动推送,避免 UI 轮询。
Usage Examples
基础用法:获取同步落库入口
任何需要写健康数据的同步模块,都通过单例拿到对应 DAO:
HealthDataDbHelper helper = HealthDataDbHelper.getInstance();
HealthDao healthDao = helper.getHealthDao(); // 健康指标
SportRecordDao sportRecordDao = helper.getSportRecordDao(); // 运动记录
LocationDao locationDao = helper.getLocationDao(); // 定位轨迹
AICloudMessageDao aiDao = helper.getAICloudMessageDao(); // AI 云端消息
Source: HealthDataDbHelper.java
进阶用法:解析设备同步命令
测试代码展示了固件同步命令的解析模式——先构造参数/响应对象,再从对象中取字段:
SportsInfoStatusSyncCmd.FirmwareStopSportsParam param =
new SportsInfoStatusSyncCmd.FirmwareStopSportsParam(data);
System.out.println("param : " + param.getFileId());
Source: StringTest.java
实时数据响应同样以命令响应对象承载,解析失败时抛 ParseDataException,调用方需捕获处理:
SportsInfoStatusSyncCmd.ReadRealDataResponse response = null;
try {
response = new SportsInfoStatusSyncCmd.ReadRealDataResponse(data);
} catch (ParseDataException e) {
// 处理解析失败(数据长度不符、校验失败等)
}
Source: StringTest.java
同步开关配置
//同步设备电量
public final static boolean SYNC_DEV_POWER = true;
...
public static final String KEY_ASSERT_RES_SYNC = "key_assert_res_sync";//更新assert资源文件
Source: HealthConstant.java 、HealthConstant.java
Configuration Options
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
HealthConstant.SYNC_DEV_POWER | boolean | true | 是否同步设备电量(编译期静态开关,改动需重新编译) |
HealthConstant.KEY_ASSERT_RES_SYNC | String | "key_assert_res_sync" | SharedPreferences 键,标记是否更新 assert 资源文件(固件资源同步) |
HealthApplication 的 Application 注入 | Context | 由 AppViewModel 提供 | HealthDataDbHelper 构造时必需;为 null 时抛 NullPointerException |
Failure Modes, Edge Cases & Concurrency
并发安全
HealthDataDbHelper.getInstance()使用volatile+synchronized双重检查锁,保证 BLE 回调线程、业务线程同时首次调用时只构建一次数据库实例;mHealthDatabase为final,构建后不可替换,避免运行期状态漂移。- Room 数据库内部对写操作串行化,多个同步任务(健康、运动、定位、AI 消息)并发写入各自 DAO 不会互相阻塞(分表分 DAO 的设计本身即是为并发同步服务)。
初始化失败
- 构造
HealthDataDbHelper时若HealthApplication.getAppViewModel().getApplication()返回 null(例如在 Application 初始化完成前调用),会立即抛出NullPointerException——这是有意为之的快速失败(fail-fast),防止后续所有同步写入静默失败。
解析失败
- 设备返回的原始字节若长度或结构不符,
FirmwareStopSportsParam/ReadRealDataResponse等构造器会抛ParseDataException,同步流程必须在 catch 中降级处理(丢弃该包、记录日志并继续后续同步),不能中断整条同步链路。
静态引用泄漏(已知取舍)
@SuppressLint("StaticFieldLeak")表明单例静态持有数据库实例是已知且接受的:以潜在 Context 泄漏风险换取全局唯一数据库连接与更低的内存开销。进程生命周期与应用一致,实际影响可控。
Performance & Operational Considerations
- 单例复用:
HealthDatabase全进程唯一,避免重复打开数据库;Room 自带连接池与查询缓存,同步写入密集场景下吞吐可控。 - 分 DAO 写入:健康/运动/定位/AI 消息分表存储,同步任务可并行、可按数据域独立调度,避免单一表锁竞争。
- VO 聚合下沉:日/月聚合(
BloodOxygenDayVo/BloodOxygenMonthVo)在查询层完成,UI 只消费结果,减少主线程计算。 - 操作建议:批量同步(如历史数据回传)应放在后台线程/协程执行,避免占用主线程;
SYNC_DEV_POWER等编译期开关变更需重新构建 App。
Extension Points
- 新增同步数据类型:参照现有模式——在 RCSP SDK 命令族中增加命令解析 → 新增/复用
HealthEntity字段 → 在HealthDao增加对应读写 → 在data/vo增加聚合 VO 并接入BaseLiveDataVo。HealthDataDbHelper与HealthDatabase骨架无需改动。 - 虚拟数据扩展:
VirtualDataHelper可扩展生成任意新数据域的模拟数据,使 UI 与测试在无设备时先行开发。 - 同步开关扩展:在
HealthConstant增加布尔开关即可按编译期控制新数据项的同步启停,与SYNC_DEV_POWER保持一致模式。
Tests
StringTest.java:验证SportsInfoStatusSyncCmd固件参数与实时数据响应的字节解析(FirmwareStopSportsParam.getFileId()、ReadRealDataResponse构造),覆盖解析成功与ParseDataException异常路径,是同步命令层正确性的回归保障。WatchManagerTest.java:验证WatchManagerByCustom、WatchManagerByJL、BleManager的单例初始化(均使用synchronized双重检查),保证同步命令收发所依赖的连接管理组件线程安全。
Related Links
- HealthDataDbHelper.java
- HealthDatabase.java
- HealthDao.java
- HealthConstant.java
- StringTest.java
- WatchManagerTest.java
- 相关兄弟页面:运动记录(SportRecord)、血氧数据(blood_oxygen 聚合)、蓝牙连接管理