健康与运动数据同步
JL_Health(JieliJianKang)中负责与 JL 穿戴设备(手环/手表)之间同步健康与运动数据的能力。它以 JLWearSync 单例为指令入口,通过 BLE 通道向设备下发运动开始/暂停/继续/结束命令、读取运动信息与实时运动数据,并通过 JLWearSyncProtocol 协议将结果回传给 App 层;App 侧进一步通过 SyncDataManager、HealthDataFormat 图表格式化组件和 UserDataSync 完成数据的展示、解析与云端上报。
Purpose and Scope
本页完整覆盖「健康与运动数据同步」这条能力链路的全部实现环节:
- 指令层:
JLWearSync提供的运动控制、运动信息读取、实时数据读取与间隔配置命令; - 回调层:
JLWearSyncProtocol协议机制(注册/移除监听、异步回调); - App 接入层:
JLPreparation中的同步初始化、JLTabBarController对运动开始协议方法的实现、AppDelegate中运动详情页的跳转逻辑; - 数据格式化层:
SyncDeviceData/HealthDataFormat下的健康图表组件(心率、血氧等); - 云端同步层:
UserDataSync用户数据上报接口。
以下内容刻意留给兄弟页面:设备搜索/连接与 OTA 升级(JLPreparation 的其余职责)、AI 云服务(语音识别/合成)、账号与基础 HTTP 封装。本页只讨论与穿戴健康/运动数据同步直接相关的部分。
Overview
穿戴设备(手表/手环)在运动过程中自行采集心率、步数、卡路里等数据,这些数据保存在设备本地。App 与设备建立 BLE 连接后,需要主动下发指令才能把数据拉到手机端:运动进行中通过「实时数据」通道按固定间隔推送,运动结束后通过「运动信息」命令一次性读取该次运动的汇总结果(JLWearSyncInfoModel)或结束回调(JLWearSyncFinishModel)。
这套设计的原因(设计意图):
- 命令-回调分离:设备端 BLE 指令天然是异步的,因此
JLWearSync采用「注册协议监听者 + block 回调」双通道:需要一次结果的命令(读运动信息、开始/暂停/继续)用 block;持续推送或状态变化(实时数据、运动结束、开始运动)走协议代理,避免一个 block 无法表达多次回调的问题。 - 单一职责入口:所有穿戴同步命令收敛到
JLWearSync单例,App 任意位置都能通过[JLWearSync share]发起命令,同时通过addProtocol:挂接自己的回调实现,形成发布-订阅关系。 - 分层格式化:原始 BLE 数据包经
SyncDeviceData层解析为模型,再由HealthDataFormat图表组件转换为可直接绘制的曲线数据,UI 层不接触协议细节。
flowchart TD
subgraph sg_App["App 层 (JieliJianKang)"]
Prep["JLPreparation"] -->|"初始化"| SDM["SyncDataManager"]
Tab["JLTabBarController"] -->|"实现 JLWearSyncProtocol"| WS["JLWearSync (单例)"]
AppDel["AppDelegate"] -->|"查询运动信息并跳转"| SportDetail["JLSportDetailViewController"]
SDM --> WS
end
subgraph sg_Sync["同步指令层 (JL_BLEKit)"]
WS -->|"BLE 指令 (RCSP)"| Device["穿戴设备"]
Device -->|"异步响应/主动上报"| Proto["JLWearSyncProtocol 回调"]
Proto --> Tab
Proto --> AppDel
end
subgraph sg_Data["数据处理层"]
Chart["HealthDataFormat 图表组件<br/>(JLWearSyncHealthChart 系列)"]
UDS["UserDataSync (云端上报)"]
Chart --> UDS
end
AppDel --> Chart
Tab --> Chart
上图中,JLWearSync 是连接 App 与设备的唯一命令通道;设备侧的结果既可以通过 block 直接回到调用方,也可以通过 JLWearSyncProtocol 广播给所有注册者(如 JLTabBarController、AppDelegate),再由它们驱动图表展示与云端上报。
核心实现:JLWearSync 指令入口
JLWearSync 定义于 JL_BLEKit 框架头文件 JLWearSync.h,注释明确其职责为「穿戴设备状态同步指令」。它是单例对象,提供协议订阅与一组运动/健康命令:
| 能力 | 方法 | 回调通道 |
|---|---|---|
| 协议订阅 | addProtocol: / removeProtocol: | — |
| 读取运动信息 | w_requireSportInfoWith:Block: | block (JL_CB_SyncSportInfo) |
| 开始运动 | w_SportStart:With:Block: | block (JL_CB_Status) |
| 结束运动 | w_SportFinishWith: | 协议 (jlWearSyncStopMotion:With:) |
| 暂停运动 | w_SportPauseWith:Block: | block (JL_CB_Status) |
| 继续运动 | w_SportContinueWith:Block: | block (JL_CB_Status) |
| 读取实时运动数据 | w_requireRealTimeSportInfoWith: | 协议 |
| 设置实时数据间隔 | pr_setTimeInterval:With: | — |
协议订阅机制
/// 加入协议响应
/// @param delegate 对象
-(void)addProtocol:(id<JLWearSyncProtocol>)delegate;
/// 移除协议响应
/// @param delegate 对象
-(void)removeProtocol:(id<JLWearSyncProtocol>)delegate;
Source: JLWearSync.h
addProtocol: 采用观察者模式:多个对象可以同时订阅同一设备的同步事件。设计上这允许「业务逻辑层」与「UI 层」各自关注自己关心的回调——例如 JLTabBarController 关心「运动开始」事件以切换页面,而 AppDelegate 关心「运动信息就绪」以跳转详情页。removeProtocol: 用于页面销毁等场景释放监听,避免野指针回调。
运动信息读取
/// 读取运动信息
/// @param entity 设备entity
/// @param block JLWearSyncInfoModel 回调
-(void)w_requireSportInfoWith:(JL_EntityM *)entity Block:(JL_CB_SyncSportInfo _Nullable)block;
Source: JLWearSync.h
该方法以 JL_EntityM(设备实体)为参数,通过 block 异步返回 JLWearSyncInfoModel(运动汇总信息,如运动 ID、运动类型、时长、卡路里等,字段细节定义在 JLWearSyncInfoModel.h)。头文件注释同时保留了一段旧协议回调签名 jlWearSyncSportInfo:,说明早期版本走纯协议回调,现版本改为 block 为主——这是向后兼容演进留下的痕迹。
运动控制命令
///开始运动命令
/// @param type 运动类型
/// 0x00 :非运动模式
/// 0x01 :室外跑步
/// 0x02 :室内跑步
/// @param entity 设备entity
/// @param block 命令成功与否回调
-(void)w_SportStart:(uint8_t)type With:(JL_EntityM *)entity Block:(JL_CB_Status _Nullable)block;
/// 结束运动命令
/// @param entity 设备entity
/// 本条命令的回调通过JLWearSyncProtocol协议
-(void)w_SportFinishWith:(JL_EntityM*)entity;
///暂停运动命令
-(void)w_SportPauseWith:(JL_EntityM*)entity Block:(JL_CB_Status _Nullable)block;
///继续运动命令
-(void)w_SportContinueWith:(JL_EntityM*)entity Block:(JL_CB_Status _Nullable)block;
Source: JLWearSync.h
注意命令设计的不对称性:开始/暂停/继续使用 JL_CB_Status block 立即确认命令是否送达;而结束运动没有 block,因为结束动作触发的是设备端的一次完整数据结算,结果(JLWearSyncFinishModel)需要等设备计算完成后通过协议回调 jlWearSyncStopMotion:With: 返回。这种区分体现了「即时命令确认」与「耗时数据结算」两种异步语义的差别。
实时数据与间隔配置
/// 读取运动实时数据
/// @param entity 设备entity
/// 本条命令的回调通过JLWearSyncProtocol协议
-(void)w_requireRealTimeSportInfoWith:(JL_EntityM *)entity;
/// 实时数据获取时间间隔设置
/// @param interval 时间间隔,单位:ms
/// @param entity 设备entity
-(void)pr_setTimeInterval:(UInt16)interval With:(JL_EntityM *)entity;
Source: JLWearSync.h
w_requireRealTimeSportInfoWith: 一旦下发,设备会按 pr_setTimeInterval: 设定的毫秒间隔持续上报实时数据(如当前心率),每次上报通过 JLWearSyncProtocol 的实时数据回调到达 App(对应 JLWearSyncRealTimeModel)。间隔参数是 UInt16 毫秒值,调用方应在开始运动前配置好;过小的间隔会增大 BLE 吞吐与功耗,属于需要按业务权衡的配置项。
App 侧接入流程
初始化:JLPreparation 与 SyncDataManager
App 启动的准备流程中会实例化数据同步管理器。在 JLPreparation.m 中:
self.isPreparateOK = 0;
[SyncDataManager share];
Source: JLPreparation.m
SyncDataManager(位于 App 工程 SyncDeviceData 目录)以单例形式在准备阶段被唤醒,作为健康/运动数据在 App 侧的总调度者——接收 JLWearSync 的协议回调、维护同步状态。同一准备流程还会在设备就绪后调用 syncDeviceTime 同步设备时间戳(JLPreparation.m 第 107 行),确保运动数据的时间基准与手机一致——时间不同步会直接导致运动记录在时间轴上错位。
开始运动:JLTabBarController 实现协议
JLTabBarController 实现了 JLWearSyncProtocol,处理「设备端开始运动」的事件:
#pragma mark - JLWearSyncProtocol
-(void)jlWearSyncStartMotionWith:(JL_EntityM *_Nonnull)entity {
[JLApplicationDelegate checkCurrentSport];
}
Source: JLTabBarController.m
当设备(例如通过快捷按键)主动进入运动模式时,JLWearSync 通过协议广播 jlWearSyncStartMotionWith:,TabBar 控制器随即调用 JLApplicationDelegate 的 checkCurrentSport 检查/切换当前运动页面。这印证了协议的多播语义:TabBar 不需要直接持有 JLWearSync,只通过 addProtocol: 订阅即可收到运动状态变化。
运动信息查询与详情页跳转:AppDelegate
在 AppDelegate.m 中,App 在收到某次运动结束后主动向设备查询运动汇总信息:
[[JLWearSync share] w_requireSportInfoWith:kJL_BLE_EntityM Block:^(JLWearSyncInfoModel *infoModel) {
[weakSelf pushSportDetailViewControllerWithWearSyncInfoModel:infoModel];
}];
Source: AppDelegate.m
- (void)pushSportDetailViewControllerWithWearSyncInfoModel:(JLWearSyncInfoModel *)infoModel {
if ((infoModel.sportID > 0) && (infoModel.sportType != 0x00) && ![JLApplicationDelegate.navigationController.viewControllers containsObject:JLApplicationDelegate.sportDetailVC]) {
JLSportDetailViewController *vc = [[JLSportDetailViewController alloc] init];
vc.wearSyncInfoModel = infoModel;
JLApplicationDelegate.sportDetailVC = vc;
}
}
Source: AppDelegate.m
这段代码揭示了三条重要业务规则:
- 数据合法性校验:
sportID > 0且sportType != 0x00才会进入详情页——sportType == 0x00表示「非运动模式」(见w_SportStart:注释),说明设备上报的空记录必须被过滤; - 页面去重:
containsObject:检查防止重复 push 同一个运动详情控制器; - 弱引用安全:block 内使用
weakSelf,避免JLWearSync持有 block 造成循环引用。
数据格式化:SyncDeviceData / HealthDataFormat
App 工程 SyncDeviceData/HealthDataFormat 目录下提供了健康数据的图表化组件:
JLWearSyncHealthChart(基类,头文件)—— 通用健康数据图表;JLWearSyncHealthHeartRateChart—— 心率曲线(头文件);JLWearSyncHealthBloodOxyganChart—— 血氧曲线(头文件)。
这些类负责把 JLWearSync 同步下来的原始健康数据转换成适合 UIKit 绘制的曲线模型,隔离了「BLE 数据格式」与「UI 展示格式」两层关注点:新增一种健康指标时,只需在该目录新增一个 JLWearSyncHealthChart 子类。
云端同步:UserDataSync
httpClient/UserDataSync.h/.m 提供用户数据的上报通道(UserDataSync.h),用于将本地同步并格式化后的健康/运动数据上传到服务端,实现多端数据一致性。它是数据链路「设备 → App → 云端」的最后一环;其实现细节(接口地址、鉴权)属于账号/HTTP 服务页面,本页不再展开。
核心流程
一次完整的「运动同步」生命周期如下图所示(从用户/设备触发运动,到数据展示与上报):
sequenceDiagram
participant U as 用户/设备按键
participant Tab as JLTabBarController
participant WS as JLWearSync (单例)
participant Dev as 穿戴设备
participant Del as AppDelegate
participant VC as JLSportDetailViewController
participant Cloud as UserDataSync
U->>WS: 触发开始运动 (w_SportStart:With:Block:)
WS->>Dev: BLE 指令
Dev-->>WS: 命令确认 (JL_CB_Status)
WS-->>Tab: 协议广播 jlWearSyncStartMotionWith:
Tab->>Tab: checkCurrentSport 切换页面
U->>WS: pr_setTimeInterval: 配置间隔 (ms)
WS->>Dev: 实时数据订阅 (w_requireRealTimeSportInfoWith:)
loop 运动中
Dev-->>WS: 按间隔上报实时数据
WS-->>Tab: JLWearSyncRealTimeModel 回调
end
U->>WS: 结束运动 (w_SportFinishWith:)
Dev-->>WS: 运动结算结果 (jlWearSyncStopMotion:With:)
WS-->>Del: 协议回调
Del->>WS: w_requireSportInfoWith:Block:
WS->>Dev: 读取运动汇总
Dev-->>WS: JLWearSyncInfoModel
WS-->>Del: block 回调
Del->>VC: pushSportDetailViewControllerWithWearSyncInfoModel: (校验 sportID/sportType)
VC->>Cloud: 格式化后经 UserDataSync 上报
流程要点:
- 两种触发路径:运动可以由 App 主动下发
w_SportStart:启动,也可以由设备侧按键启动(此时 App 侧只收到协议广播)。两种路径最终都汇聚到JLWearSync单例,保证状态机唯一; - 实时数据是推送流:
w_requireRealTimeSportInfoWith:订阅后,回调可能发生多次,因此只能走协议代理而不能用单次 block; - 结束是两段式:
w_SportFinishWith:只负责触发结算,真正的数据通过协议回调jlWearSyncStopMotion:With:返回;之后 App 还要再发一次w_requireSportInfoWith:才能拿到用于详情页展示的JLWearSyncInfoModel; - 展示前有强校验:
sportID、sportType校验 + 控制器去重,防止空记录和重复页面。
使用示例
示例 1:运动结束后查询运动信息并跳转详情页
这是 App 中最典型的「读运动信息」用法——在 block 中拿到 JLWearSyncInfoModel,校验后驱动 UI 跳转:
[[JLWearSync share] w_requireSportInfoWith:kJL_BLE_EntityM Block:^(JLWearSyncInfoModel *infoModel) {
[weakSelf pushSportDetailViewControllerWithWearSyncInfoModel:infoModel];
}];
Source: AppDelegate.m
示例 2:实现 JLWearSyncProtocol 监听运动开始
任何需要感知设备运动状态的对象都可以注册为协议监听者,并在回调中响应:
#pragma mark - JLWearSyncProtocol
-(void)jlWearSyncStartMotionWith:(JL_EntityM *_Nonnull)entity {
[JLApplicationDelegate checkCurrentSport];
}
Source: JLTabBarController.m
结合 JLWearSync 的 addProtocol: / removeProtocol:,业务方可实现「订阅-响应-退订」的标准观察者生命周期。
示例 3:准备流程中初始化同步管理器
App 启动准备阶段唤醒同步组件,为后续命令执行建立好回调环境:
self.isPreparateOK = 0;
[SyncDataManager share];
Source: JLPreparation.m
示例 4:下发开始运动命令(含运动类型)
调用方按业务选择运动类型(0x01 室外跑步 / 0x02 室内跑步),通过 JL_CB_Status block 获取命令是否成功送达的即时确认:
-(void)w_SportStart:(uint8_t)type With:(JL_EntityM *)entity Block:(JL_CB_Status _Nullable)block;
Source: JLWearSync.h
配置选项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
运动类型 type(w_SportStart:) | uint8_t | 无 | 0x00 非运动模式;0x01 室外跑步;0x02 室内跑步 |
实时数据间隔 interval(pr_setTimeInterval:) | UInt16 | 由调用方设置 | 单位 ms,决定设备实时数据上报频率 |
设备实体 entity | JL_EntityM * | 当前连接设备(kJL_BLE_EntityM) | 所有同步命令的目标设备句柄 |
协议监听(addProtocol:) | id<JLWearSyncProtocol> | 无 | App 侧可同时注册多个监听者 |
说明:
kJL_BLE_EntityM为 App 工程中指向当前已连接设备实体的宏/全局引用(见 AppDelegate.m 第 179 行)。运动类型取值以JLWearSync.h头文件注释为准。
API Reference
+ (instancetype)share
获取 JLWearSync 单例。所有同步命令与协议注册均通过该实例发起。
- (void)addProtocol:(id<JLWearSyncProtocol>)delegate
注册协议监听者。设备侧的状态变化(运动开始、运动结束、实时数据)会广播给所有已注册的 delegate。
参数: delegate(id<JLWearSyncProtocol>)—— 实现同步协议的对象。
- (void)removeProtocol:(id<JLWearSyncProtocol>)delegate
移除协议监听者。页面销毁或不再关心同步事件时必须调用,防止对已释放对象的回调。
- (void)w_requireSportInfoWith:(JL_EntityM *)entity Block:(JL_CB_SyncSportInfo)block
读取设备的运动汇总信息。
参数:
entity(JL_EntityM *):目标设备;block(JL_CB_SyncSportInfo):异步回调,携带JLWearSyncInfoModel。
返回: 无(结果经 block 返回)。
注意: 若 infoModel.sportID <= 0 或 sportType == 0x00,表示无有效运动记录,App 侧应过滤(见 AppDelegate.m)。
- (void)w_SportStart:(uint8_t)type With:(JL_EntityM *)entity Block:(JL_CB_Status)block
下发开始运动命令。
参数:
type(uint8_t):运动类型,0x01室外跑步、0x02室内跑步、0x00退出运动模式;entity(JL_EntityM *):目标设备;block(JL_CB_Status):命令送达确认回调。
- (void)w_SportFinishWith:(JL_EntityM *)entity
下发结束运动命令。结束后的运动结算数据通过协议回调 jlWearSyncStopMotion:With: 返回(JLWearSyncFinishModel),因此本方法无 block。
- (void)w_SportPauseWith:(JL_EntityM *)entity Block:(JL_CB_Status)block
暂停当前运动,block 返回命令送达确认。
- (void)w_SportContinueWith:(JL_EntityM *)entity Block:(JL_CB_Status)block
继续被暂停的运动,block 返回命令送达确认。
- (void)w_requireRealTimeSportInfoWith:(JL_EntityM *)entity
订阅运动实时数据。订阅后设备按 pr_setTimeInterval: 设定的间隔持续上报,每次上报通过协议回调送达(JLWearSyncRealTimeModel)。注意该回调会多次触发,不能使用单次 block 语义。
- (void)pr_setTimeInterval:(UInt16)interval With:(JL_EntityM *)entity
设置实时数据上报间隔。
参数: interval(UInt16)—— 毫秒值。应在订阅实时数据前调用;间隔越小数据越密集,BLE 负载与功耗越高。
失败模式、边界情况与并发
以下分析基于已阅读的源码与头文件注释,未在源码中直接看到的细节已明确标注为推断。
无有效运动记录
w_requireSportInfoWith: 的 block 可能返回 sportID <= 0 或 sportType == 0x00 的记录(例如用户未真正运动就退出)。App 侧在 pushSportDetailViewControllerWithWearSyncInfoModel: 中做了显式过滤,任何调用方在展示运动详情前都应重复该校验,否则会产生无意义的空详情页。
协议监听者生命周期
JLWearSync 使用广播式回调(协议代理)。若对象在 addProtocol: 之后被释放却未调用 removeProtocol:,设备后续上报将触发对已释放对象的调用,导致野指针崩溃。建议在 viewWillDisappear/dealloc 中成对调用 addProtocol:/removeProtocol:。
block 循环引用
w_requireSportInfoWith:Block: 的 block 会被框架持有直到回调完成。若 block 内部强引用 self(如 AppDelegate),将形成 JLWearSync → block → self 的引用环。源码中的正确做法是使用 __weak typeof(self) weakSelf(见 AppDelegate.m 第 178 行)。
运动状态机冲突(推断)
运动开始/暂停/继续/结束之间存在互斥关系(例如设备已在运动时重复下发 w_SportStart:,或未开始时下发 w_SportFinishWith:)。JLWearSync 头文件未暴露状态查询接口,命令的即时确认仅表示「已送达」,设备端是否接受该命令以 JL_CB_Status 与实际协议回调为准,业务层应自行维护运动状态机,避免在错误时序下发送命令。
断连与超时(推断)
所有命令都依赖 BLE 连接。若命令发送时设备已断开,block 回调可能永远不会触发。源码层面未见重试或超时机制暴露,建议业务层对关键命令(开始/结束运动)增加超时保护与断连检测,并与设备连接管理模块(兄弟页面)协同处理。
并发/多回调
实时数据回调是高频事件流。若 UI 层直接在回调线程刷新图表,可能造成主线程卡顿或数据竞争;HealthDataFormat 图表组件将数据转换为绘制模型,UI 侧应把渲染调度到主线程。
性能与运维注意事项
- 实时数据间隔权衡:
pr_setTimeInterval:的UInt16毫秒值直接决定 BLE 吞吐。高频(如 100ms 以下)适合运动过程中的心率监控,但会增加功耗与丢包概率;低频(如 1000ms)适合长时间待机记录。App 应根据运动类型动态配置。 - 数据流是单向管道:从设备实时上报 → 协议广播 → 图表格式化 → 云端上报,每一层都做格式转换;
HealthDataFormat把「协议模型」与「UI 模型」解耦,避免 UI 层耦合 BLE 协议。 - 启动时序:
SyncDataManager在JLPreparation准备阶段初始化、syncDeviceTime同步时间戳,说明「时间基准同步」是数据正确性的前置条件,运维排查运动记录时间错乱时应首先检查该步骤是否成功。
扩展点
JLWearSyncProtocol协议实现:任何对象实现该协议并addProtocol:注册即可接收运动状态与实时数据,是 App 侧扩展运动业务(如后台统计、消息推送)的标准入口。当前 App 中JLTabBarController(运动开始)与AppDelegate(运动结束/信息)是典型实现者。JLWearSyncCustom:框架提供 JLWearSyncCustom.h,用于自定义同步扩展命令(具体能力以该头文件为准)。HealthDataFormat图表子类化:新增健康指标(如体温、睡眠分期)时,在SyncDeviceData/HealthDataFormat下继承JLWearSyncHealthChart并实现对应的数据转换逻辑,即可复用现有同步链路。UserDataSync上报通道:健康数据经格式化后通过UserDataSync上报云端,可在此基础上扩展多设备同步、历史数据拉取等能力。
测试情况
在本次勘察的源码范围内,未发现针对健康与运动数据同步模块的独立测试文件(工程以 .m 业务实现为主,未见 XCTest 用例)。该模块的正确性主要依赖真机 BLE 联调验证。若后续补充测试,建议优先覆盖:运动记录合法性过滤(sportID/sportType 校验)、协议回调时序(开始→实时→结束)、以及 HealthDataFormat 对空数据/异常值的容错。
相关链接
- JLWearSync.h(同步指令头文件)
- JL_WatchSyncProtocol.h(同步回调协议)
- JLWearSyncInfoModel.h(运动信息模型)
- JLWearSyncFinishModel.h(运动结束模型)
- JLWearSyncRealTimeModel.h(实时数据模型)
- JLWearSyncCustom.h(自定义同步扩展)
- AppDelegate.m(运动信息查询与详情页跳转)
- JLPreparation.m(同步初始化与设备时间同步)
- JLTabBarController.m(运动开始协议实现)
- SyncDeviceData/HealthDataFormat(健康图表格式化组件)
- UserDataSync.h(云端数据上报)
- 设备连接与 OTA 流程:见「设备管理/OTA」相关页面;AI 语音服务:见「AI 云服务」相关页面