杰理 SDK 文档中心
首页
首页
  • 项目概述

    • 项目简介与核心能力
    • 运行环境与SDK版本
  • 快速开始

    • 工程导入与依赖配置
    • 权限配置与示例运行
  • 平台架构

    • SDK分层架构与RCSP协议
    • 蓝牙连接库
    • 健康SDK核心库 JL_Watch
    • 健康服务器与云端服务
  • 健康与运动数据

    • 健康数据同步
    • 运动数据同步
    • 本地数据持久化
  • 设备管理功能

    • 表盘管理
    • 闹钟与健康提醒
    • 消息与联系人同步
    • 天气同步
    • 设备查找
    • 支付宝集成
  • 传输与媒体处理

    • 文件传输与文件管理
    • 音乐传输与播放控制
    • 图像转换库
    • 音频编解码与解密
  • OTA 升级

    • 固件空中升级流程
    • 4G模块与差分升级
  • AI 能力

    • AI表盘与云服务
    • AI语音助手
  • 示例应用

    • HealthAide 健康助手应用
    • WatchTestTool 测试工具
  • 开发者指南

    • 自定义命令扩展
    • 调试技巧与问题排查
    • 版本历史与兼容性

本地数据持久化

HealthAide 应用基于 Android Room 框架构建的本地 SQLite 持久化层,负责健康指标、运动记录、位置轨迹、用户信息与 AI 云端消息等数据的落盘存储、查询与迁移。

Purpose and Scope

本页讲解 HealthAide(HealthAide_V1.1.0_SDK_V1.14.0 模块)中 com.jieli.healthaide.data 包下的本地持久化机制,涵盖:

  • 数据库层(data/db):Room 数据库 HealthDatabase、数据库门面 HealthDataDbHelper、演示数据辅助类 VirtualDataHelper;
  • 数据访问层(data/dao):UserDao、SportRecordDao、HealthDao、LocationDao、AICloudMessageDao;
  • 实体层(data/entity):User、SportRecord、HealthEntity、LocationEntity、AICloudMessageEntity;
  • 数据库版本与迁移策略:版本 3、MIGRATION_2_3 增量建表;
  • 线程模型与生命周期:单例初始化、单线程写入线程池、destroy() 清理。

本页不覆盖数据来源侧的蓝牙协议解析(VO 解析层)、云端 AI 接口调用或 UI 层展示逻辑,这些内容属于 4-health-sports 目录下的其他页面。

Overview

为什么需要本地持久化

健康类应用的核心场景是持续采集、离线可查、历史回溯:心率、血氧、步数、运动记录、GPS 轨迹等数据由手环/手表通过蓝牙持续同步到手机,若仅保存在内存中,应用进程被杀后数据即丢失;若全部依赖云端,则网络不可用时功能不可用。因此 HealthAide 将采集数据先落入本地 SQLite 数据库(jl_health.db),再按需上传或展示。

为什么选择 Room

  • 编译期校验:@Database / @Entity 注解与 DAO 接口在编译期生成 SQL 访问实现,表结构或 SQL 错误在编译期即可暴露;
  • 迁移机制:通过 Migration 类控制 schema 演进,避免升级时数据丢失;
  • 与 LiveData/协程集成友好:DAO 返回值可直接支撑 UI 层响应式刷新(data/vo 包中另有 VO 解析体系配合使用)。

关键概念

概念说明
HealthDatabaseRoom 数据库抽象类,唯一数据源,管理 5 张表
DAO(Data Access Object)每张表对应的数据访问接口,封装 SQL 增删改查
Entity与表一一对应的 POJO 类
Migration数据库版本升级时的增量 schema 变更
HealthDataDbHelper面向业务层的单例门面,统一暴露 DAO 与数据库实例

Architecture

flowchart TD
    subgraph sg_App["应用上层"]
        VM["ViewModel / 业务层"]
        App["HealthApplication"]
    end

    subgraph sg_Facade["访问门面"]
        Helper["HealthDataDbHelper<br/>(单例门面)"]
    end

    subgraph sg_Db["数据库层 data/db"]
        DB["HealthDatabase<br/>(Room 单例, jl_health.db, version=3)"]
        VP["VirtualDataHelper<br/>(演示数据)"]
    end

    subgraph sg_Dao["数据访问层 data/dao"]
        UDao["UserDao"]
        SDao["SportRecordDao"]
        HDao["HealthDao"]
        LDao["LocationDao"]
        ADao["AICloudMessageDao"]
    end

    subgraph sg_Entity["实体层 data/entity"]
        U["User"]
        S["SportRecord"]
        H["HealthEntity"]
        L["LocationEntity"]
        A["AICloudMessageEntity"]
    end

    SQLite[("SQLite 文件<br/>jl_health.db")]

    VM -->|"getInstance()"| Helper
    App -->|"提供 Application Context"| Helper
    Helper -->|"getHealthDatabase()"| DB
    Helper -->|"getXxxDao()"| UDao
    Helper -->|"getXxxDao()"| SDao
    Helper -->|"getXxxDao()"| HDao
    Helper -->|"getXxxDao()"| LDao
    Helper -->|"getXxxDao()"| ADao
    DB --> UDao
    DB --> SDao
    DB --> HDao
    DB --> LDao
    DB --> ADao
    UDao --> U
    SDao --> S
    HDao --> H
    LDao --> L
    ADao --> A
    U --> SQLite
    S --> SQLite
    H --> SQLite
    L --> SQLite
    A --> SQLite
    VP -.->|"预置/演示数据"| DB

架构说明:上层业务(ViewModel 等)从不直接接触 Room,而是通过 HealthDataDbHelper 单例获取 DAO 或数据库实例。这一分层的目的:

  1. 隔离实现细节:数据库构建、迁移、线程池管理都被封装在 data/db 内部,业务层只面对类型安全的 DAO 接口;
  2. 单一入口:HealthDataDbHelper 是唯一对外门面,DAO 实例由 HealthDatabase 统一生成,避免多处重复构建数据库;
  3. 便于替换:若未来切换 DataStore 或自研存储,只需改动门面与数据库层,DAO 签名不变,上层无感知。

核心实现

1. Room 数据库:HealthDatabase

HealthDatabase 是持久化层的核心抽象类,通过 @Database 注解声明全部实体与当前版本号:

@Database(entities = {User.class, SportRecord.class,HealthEntity.class, LocationEntity.class, AICloudMessageEntity.class}, version = 3)
public abstract class HealthDatabase extends RoomDatabase {
    private final static String DB_NAME = "jl_health.db";
    private volatile static HealthDatabase instance;
    private final ExecutorService mThreadPool = Executors.newSingleThreadExecutor();

    public abstract UserDao UserDao();
    public abstract SportRecordDao SportRecordDao();
    public abstract HealthDao HealthDao();
    public abstract LocationDao LocationDao();
    public abstract AICloudMessageDao AICloudMessageDao();
    ...
}

Source: HealthDatabase.java

设计要点:

  • 一张数据库文件承载五张表:健康指标(HealthEntity)、运动记录(SportRecord)、位置(LocationEntity)、用户(User)、AI 云端消息(AICloudMessageEntity),统一版本号 3,所有迁移在同一数据库上按版本递增执行;
  • DAO 以抽象方法暴露:Room 在编译期根据方法返回类型(UserDao 等)自动生成实现,接口即契约;
  • 内置单线程 Executor:mThreadPool = Executors.newSingleThreadExecutor() 为写入操作提供串行化执行通道,避免多线程并发写同一 SQLite 文件时的锁竞争与 SQLiteDatabaseLockedException。

单例构建(双重检查锁)

public static HealthDatabase buildHealthDb(Context context) {
    if (instance == null) {
        synchronized (HealthDatabase.class) {
            if (instance == null) {
                instance = Room.databaseBuilder(context, HealthDatabase.class, DB_NAME)
                        .allowMainThreadQueries()
//                            .createFromAsset("databases/jl_health.db")//todo 测试使用的预存健康数据
                        .addMigrations(MIGRATION_2_3)
                        .build();
            }
        }
    }
    return instance;
}

Source: HealthDatabase.java

设计意图分析:

  • 双重检查锁(DCL):volatile 修饰的 instance 配合 synchronized 块,保证多线程环境下数据库只被构建一次,同时避免每次访问都加锁的开销;
  • allowMainThreadQueries():显式放开主线程查询限制。这是为兼容历史代码中直接在主线程读取健康数据的调用(如部分 UI 初始化路径),代价是可能产生主线程 I/O 卡顿——这是权衡而非最佳实践,新代码应尽量使用线程池(见下);
  • createFromAsset 被注释:保留了一份"预存健康数据"的测试路径,正式构建时不使用;
  • addMigrations(MIGRATION_2_3):注册版本 2→3 的迁移,Room 在打开数据库时若发现磁盘版本与期望版本不一致,会自动按注册的迁移链升级。

线程池与销毁

public ExecutorService getThreadPool() {
    return mThreadPool;
}

public void destroy() {
    if (!mThreadPool.isShutdown()) {
        mThreadPool.shutdownNow();
    }
    instance.close();
    instance = null;
}

Source: HealthDatabase.java

destroy() 是完整的生命周期收尾:先 shutdownNow() 中断排队/执行中的写任务,再 close() 关闭数据库连接,最后置空单例以便下次 buildHealthDb 重建。该设计支持应用在账号切换或登出时安全重置本地数据源。

2. 数据库迁移:MIGRATION_2_3

/**
 * 数据库版本 2->3 增加了 AICloudMessageEntity 表
 */
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE IF NOT EXISTS `AICloudMessageEntity` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT NOT NULL, `devMac` TEXT NOT NULL, `role` INTEGER NOT NULL, `time` INTEGER NOT NULL, `revId` INTEGER NOT NULL, `aiCloudState` INTEGER NOT NULL, `text` TEXT)");
    }
};

Source: HealthDatabase.java

该迁移展示了本地持久化的演进策略:只增不删。新增 AI 云端消息表时使用 CREATE TABLE IF NOT EXISTS 保证幂等(迁移失败或重复执行不会报错),且不触碰已有四张表的任何列,避免老用户升级时数据损坏。从建表 SQL 可以还原 AICloudMessageEntity 的完整结构:

列名类型约束/含义
idINTEGER主键,自增
uidTEXT用户 ID(非空)
devMacTEXT设备 MAC(非空)
roleINTEGER消息角色(非空)
timeINTEGER时间戳(非空)
revIdINTEGER云端回复 ID(非空)
aiCloudStateINTEGERAI 云端状态(非空)
textTEXT消息文本(可空)

3. 访问门面:HealthDataDbHelper

HealthDataDbHelper 是业务层访问数据库的统一入口,采用饿汉式单例 + 静态持有模式:

@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

要点:

  • Context 来源:从 HealthApplication.getAppViewModel().getApplication() 获取全局 Application Context(而非 Activity),避免内存泄漏;若拿不到则抛出 NullPointerException 快速失败——这保证了数据库实例一定在合法环境下构建;
  • @SuppressLint("StaticFieldLeak"):承认静态持有 Context 的固有风险,但 Application 级 Context 生命周期与应用进程一致,实际不会泄漏;
  • 对外方法:getHealthDao()、getSportRecordDao()、getLocationDao()、getAICloudMessageDao() 分别返回对应 DAO,getHealthDatabase() 返回数据库本体(供需要直接操作线程池/事务的场景使用):
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

4. DAO 与实体全景

data/dao 与 data/entity 两个包构成了"每实体一张表、每表一个 DAO"的规整映射:

DAO对应实体存储内容
UserDaoUser用户档案/账号信息
SportRecordDaoSportRecord运动记录(类型、时长、消耗等)
HealthDaoHealthEntity健康指标(心率、血氧等按时间序列存储)
LocationDaoLocationEntity运动轨迹 GPS 定位点
AICloudMessageDaoAICloudMessageEntityAI 云端对话消息(含 role/revId/aiCloudState 字段)

每个 DAO 均通过 @Dao 注解定义插入、查询、更新、删除方法,由 Room 编译期生成实现;实体类通过 @Entity 注解声明表名与主键。查询结果通常直接支撑 UI:健康数据与运动记录按时间区间(日/月)聚合展示,位置数据按轨迹回放,AI 消息按会话渲染——本地持久化是整个"健康-运动"业务闭环的数据底座。

5. 演示数据:VirtualDataHelper

data/db 包中还存在 VirtualDataHelper,用于生成/注入演示数据(与 HealthDatabase 中被注释的 createFromAsset("databases/jl_health.db") 测试路径呼应),方便在无真实设备时验证列表、图表与轨迹展示。具体实现细节建议直接查阅该文件。

核心流程

写入流程(数据落盘)

sequenceDiagram
    participant Biz as 业务层 ViewModel
    participant Helper as HealthDataDbHelper
    participant DB as HealthDatabase
    participant DAO as XxxDao
    participant SQLite as jl_health.db

    Biz->>Helper: getInstance()
    Helper->>DB: 内部构造时 buildHealthDb(context)
    Note over DB: Room 构建实例<br/>校验版本、执行 MIGRATION_2_3
    Biz->>Helper: getXxxDao()
    Helper-->>Biz: DAO 实例
    Biz->>DB: getThreadPool().execute(...)
    DB-->>Biz: 单线程 Executor
    Biz->>DAO: insert(entity) / update(entity)
    DAO->>SQLite: 生成 SQL 写入
    SQLite-->>DAO: 行 id / 影响行数
    DAO-->>Biz: 结果(id / 行数)

读取流程(数据展示)

sequenceDiagram
    participant UI as UI/图表
    participant Biz as 业务层
    participant Helper as HealthDataDbHelper
    participant DAO as HealthDao/SportRecordDao
    participant SQLite as jl_health.db

    UI->>Biz: 请求某时间段健康数据
    Biz->>Helper: getHealthDao()
    Helper-->>Biz: HealthDao
    Biz->>DAO: 按时间区间查询(日/月聚合)
    DAO->>SQLite: SELECT ... WHERE time BETWEEN ...
    SQLite-->>DAO: 结果集
    DAO-->>Biz: Entity / VO 列表
    Biz-->>UI: 数据驱动图表刷新

流程说明:所有写入都通过 HealthDatabase.getThreadPool() 单线程执行器串行提交,保证 SQLite 写入的顺序一致性;读取路径则经门面取 DAO 后直接查询。主线程查询因 allowMainThreadQueries() 被允许,但建议新代码遵循"写走线程池、读异步化"的约定,避免阻塞 UI。

使用示例

示例一:初始化数据库并写入健康数据

// 业务层获取数据库与线程池,在后台线程写入健康指标
HealthDatabase db = HealthDataDbHelper.getInstance().getHealthDatabase();
db.getThreadPool().execute(() -> {
    HealthDao dao = HealthDataDbHelper.getInstance().getHealthDao();
    dao.insert(healthEntity);   // 插入一条健康记录
    dao.update(healthEntity);   // 或更新已有记录
});

Source: HealthDataDbHelper.java 与 HealthDatabase.java

示例二:数据库版本迁移(2 → 3 新增 AI 消息表)

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE IF NOT EXISTS `AICloudMessageEntity` "
                + "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT NOT NULL, "
                + "`devMac` TEXT NOT NULL, `role` INTEGER NOT NULL, `time` INTEGER NOT NULL, "
                + "`revId` INTEGER NOT NULL, `aiCloudState` INTEGER NOT NULL, `text` TEXT)");
    }
};
// 注册方式:Room.databaseBuilder(context, HealthDatabase.class, "jl_health.db")
//            .addMigrations(MIGRATION_2_3).build();

Source: HealthDatabase.java

配置选项

配置项类型/取值默认值说明
数据库文件名 DB_NAMEString"jl_health.db"SQLite 数据库文件名,存储在应用私有目录
数据库版本 versionint3通过 @Database(version = 3) 声明,变更 schema 时必须递增并注册 Migration
实体列表 entitiesClass[]{User, SportRecord, HealthEntity, LocationEntity, AICloudMessageEntity}@Database 注解声明,决定建表
主线程查询booleantrue(allowMainThreadQueries())允许在主线程执行查询;历史兼容性开关
预置数据库 createFromAssetString path未启用(已注释)从 assets 拷贝预存数据库,测试专用
迁移 addMigrationsMigration[]MIGRATION_2_3版本 2→3 增量建表
写线程池ExecutorService单线程 newSingleThreadExecutor()串行化所有写入任务,避免并发锁冲突

API 参考

HealthDatabase

方法签名说明
构建static HealthDatabase buildHealthDb(Context context)双重检查锁单例构建;重复调用返回同一实例
用户 DAOabstract UserDao UserDao()Room 生成的 DAO 访问器
运动 DAOabstract SportRecordDao SportRecordDao()运动记录访问器
健康 DAOabstract HealthDao HealthDao()健康指标访问器
位置 DAOabstract LocationDao LocationDao()GPS 轨迹访问器
AI 消息 DAOabstract AICloudMessageDao AICloudMessageDao()AI 云端消息访问器
线程池ExecutorService getThreadPool()返回单线程写入执行器
销毁void destroy()关闭线程池与数据库并置空单例

HealthDataDbHelper

方法签名说明
单例static HealthDataDbHelper getInstance()双重检查锁;构造时校验 Application Context 非空
健康 DAOHealthDao getHealthDao()委托 mHealthDatabase.HealthDao()
运动 DAOSportRecordDao getSportRecordDao()委托 mHealthDatabase.SportRecordDao()
位置 DAOLocationDao getLocationDao()委托 mHealthDatabase.LocationDao()
AI 消息 DAOAICloudMessageDao getAICloudMessageDao()委托 mHealthDatabase.AICloudMessageDao()
数据库HealthDatabase getHealthDatabase()返回数据库本体(线程池/事务等高级场景)

构造异常:HealthDataDbHelper() 在 HealthApplication.getAppViewModel().getApplication() 返回 null 时抛出 NullPointerException("Application is null."),用于在应用初始化顺序错误时快速暴露问题。

失败模式、边界情况与并发

并发与一致性

  • 写入串行化:HealthDatabase 内置 newSingleThreadExecutor(),所有写操作(insert/update/delete)应通过 getThreadPool() 提交,从根本上避免 SQLite 多写并发导致的 SQLiteDatabaseLockedException;
  • 单例双重检查锁:instance 使用 volatile 修饰,buildHealthDb 与 getInstance 均采用 DCL,保证多线程下只构建一次;Room 自身对同一数据库文件也做了进程内单例约束;
  • 读多写少:查询不经过线程池(allowMainThreadQueries 放开主线程读),UI 直接驱动读取,写入走后台,读写互不阻塞。

生命周期与销毁边界

  • destroy() 语义:先 shutdownNow() 中断线程池再 close() 数据库。若在销毁后仍有代码调用 getInstance(),会重新构建一个全新数据库实例——上层必须保证"销毁后不再持有旧 DAO 引用",否则会得到已关闭的数据库(IllegalStateException: attempt to re-open an already-closed object);
  • 进程级单例生命周期:HealthDataDbHelper 与 HealthDatabase 均为静态单例,生命周期等同应用进程;HealthApplication 是 Context 的唯一合法来源,任何使用 Activity/Service Context 构建数据库的调用都是隐患。

数据完整性风险

  • 迁移只增不删:MIGRATION_2_3 使用 CREATE TABLE IF NOT EXISTS,幂等但不处理已有数据清洗;若后续版本修改列约束(如将可空列改为非空),必须新增迁移做 UPDATE ... WHERE 数据回填,否则升级后可能出现约束冲突;
  • 版本号与迁移不匹配:@Database(version = 3) 只注册了 MIGRATION_2_3。若磁盘上存在版本 1 的旧库,Room 将抛出 IllegalStateException(缺少 1→2 迁移链)——这意味着版本 1 的库已被放弃支持,属于有意的边界;
  • 外键/级联缺失:从迁移 SQL 可见表间无外键约束,实体间关联(如 SportRecord 与 LocationEntity 的轨迹归属)由业务层维护,删除运动记录时需手动级联清理位置数据。

边界情况

  • text 可空:AICloudMessageEntity.text 无 NOT NULL 约束,渲染 AI 消息时需判空;
  • 时间戳为 INTEGER:time 以毫秒/秒级整数存储,查询"日/月"聚合依赖 WHERE time BETWEEN 的区间过滤,时区换算由上层 VO/UI 处理;
  • 空数据:首次安装无任何数据,DAO 查询返回空列表,UI 需有空态设计(VirtualDataHelper 提供演示数据填充路径)。

性能与运维注意事项

  • 主线程 I/O:allowMainThreadQueries() 允许主线程查询,简单点查(单条记录)可接受,但日/月聚合、轨迹回放等大数据量查询不应在主线程执行,应使用线程池或异步查询,否则列表滚动/图表绘制会卡顿;
  • 写入批量性:健康数据为高频时间序列(心率、血氧可能每分钟多条),建议批量插入(DAO 的 insertAll 风格方法)并复用同一事务,减少 SQLite 文件写放大;
  • 数据库体积:长时间佩戴会积累大量 HealthEntity 与 LocationEntity 记录,需在业务层制定保留策略(如按月清理),当前源码未体现自动清理任务;
  • 调试与测试:被注释的 createFromAsset("databases/jl_health.db") 与 VirtualDataHelper 表明团队使用"预置数据库 + 演示数据"两条路径做无设备联调;接入 CI 时可利用这两条路径做 UI 冒烟测试。

扩展点

  • 新增实体表:按现有模式操作——新增 @Entity 类 → 新建 @Dao 接口 → 在 HealthDatabase 的 entities 数组与抽象方法中各加一项 → 版本号 +1 → 注册 Migration(old, new) 增量建表 → 在 HealthDataDbHelper 暴露对应 DAO 门面方法;
  • 切换存储方案:业务层只依赖 HealthDataDbHelper 门面与 DAO 签名,若未来迁移至 DataStore/MMKV 或自研缓存,可替换 data/db 与 data/dao 两包实现而不影响上层;
  • 预置数据:取消 createFromAsset 注释并放置 app/src/main/assets/databases/jl_health.db 即可发布带初始数据的版本(适合演示包);
  • 会话级数据重置:账号切换时可调用 HealthDatabase.destroy() 后重建,或按 uid/devMac 字段做分区清理。

测试现状

仓库中存在 app/src/androidTest 下的 ExampleInstrumentedTest.java(仪器化测试模板),未发现针对数据库层(DAO/迁移/实体)的专门单元测试。建议补充以下用例以固化行为:

  • MIGRATION_2_3:用版本 2 的预置库升级,断言 AICloudMessageEntity 表存在且旧四表数据完好;
  • DAO CRUD:插入/查询/更新/删除的往返一致性(含 time 区间过滤);
  • 单例语义:多次 getInstance() 返回同一实例,destroy() 后可重建。

相关链接

  • 数据解析层(VO 体系):com.jieli.healthaide.data.vo(蓝牙原始数据 → 业务对象解析,与实体持久化配套)
  • 应用入口:HealthApplication(提供 getAppViewModel().getApplication() 作为数据库 Context 来源)
  • 演示数据:data/db/VirtualDataHelper.java
  • 数据库门面:data/db/HealthDataDbHelper.java
  • 数据库定义:data/db/HealthDatabase.java
Prev
运动数据同步