杰理 SDK 文档中心
首页
首页
  • 概述与快速开始

    • 仓库概览
    • 运行环境与 SDK 集成
    • 工程结构与目录导航
  • 核心 SDK 架构

    • SDK 库体系与模块划分
    • 蓝牙连接与 RCSP 协议
    • 广播包解析与设备认证
    • 日志助手与调试支持
  • 设备功能模块

    • OTA 固件升级
    • 表盘管理与自定义表盘
    • 图像转换工具
    • 资源打包
    • 音频编解码
    • 健康与运动数据同步
    • 消息通知与实用设备功能
  • 宜动健康示例应用

    • 应用架构与页面导航
    • 健康界面与数据可视化
    • 设备连接与数据同步
    • 登录注册与用户中心
    • AI 云服务与语音交互
    • 本地数据库与持久化
    • 多语言国际化
  • 测试与调试

    • SDKTestHelper 功能测试工具
    • 音频编解码示例工程
    • 调试技巧与问题排查
  • 文档与资源

    • 在线文档与版本历史
    • 第三方框架与依赖管理

本地数据库与持久化

本文档说明 iOS-JL_Health 项目中设备列表、用户配置、表盘状态等数据的本地持久化机制,涵盖 SQLite 设备数据库(JLDeviceSqliteManager)、UserDefaults 轻量存储、plist 资源配置读取与表盘本地缓存,以及它们在上层 ViewModel 中的完整读写流程。

Purpose and Scope

本页面聚焦于 App 侧的本地数据持久化能力,回答以下问题:

  • 设备列表数据如何写入/读取本地 SQLite 数据库(通过 SDK 提供的 JLDeviceSqliteManager 单例);
  • UserDefaults 在测试工具中如何保存 WiFi 配置等轻量键值数据;
  • plist 资源文件如何被解码为 Swift 字典;
  • 表盘(Dial)的当前选中状态与已下载列表如何通过 BridgeHelper.dialCache() 在本地维护;
  • 上层 ViewModel 如何编排「本地读取 → 服务器同步 → 本地回写 → UI 刷新」的完整数据流。

以下相关主题不在本页面范围内,请参见对应页面:

  • 蓝牙设备连接与会话管理:由 SDK(BridgeHelper、JLDeviceSqliteManager 的实现)负责,本页面仅覆盖仓库侧的使用方式;
  • 网络请求与服务器同步:见网络层相关页面(DeviceHttp、DialMarketHttp);
  • 表盘市场业务:见「表盘市场」相关页面。

Overview

iOS-JL_Health 是一个健康类 App,核心业务围绕蓝牙设备的绑定、连接与管理展开。设备列表是该业务的关键状态:用户可能在不同时间绑定多台设备(手表/手环),App 必须保证设备列表在冷启动后立即可用,即使此时网络不可用。

为此,项目采用了「本地优先(Local-First)」的持久化策略:

  1. SQLite 设备数据库:SDK 提供 JLDeviceSqliteManager(单例),将用户设备列表持久化到 SQLite,按用户身份标识(userIdentify)隔离数据;
  2. UserDefaults:测试工具 SDKTestHelper 使用系统级键值存储保存 WiFi SSID/密码等测试参数;
  3. plist 资源:通过 PropertyListDecoder 将打包进 Bundle 的 CodeExample.plist 解码为配置字典;
  4. 表盘缓存:BridgeHelper.dialCache() 维护当前表盘名称与本地已下载表盘列表(对应手表文件系统中的表盘文件)。

设计意图:把「设备列表」等关键数据放在本地 SQLite,使 App 启动即可渲染历史设备,再异步与服务器对账(queryServerDevices),既保证了离线可用性,又保证了多端一致性。UserDefaults 与 plist 则服务于「低价值、小体积」的配置类数据,避免为它们引入数据库开销。

Architecture

下图展示了本地持久化在 App 中的分层结构与数据流向:

flowchart TD
    subgraph sg_UI["UI 层"]
        VC["DialHistoryViewController / 设备列表页"]
    end

    subgraph sg_VM["ViewModel 层"]
        SubVM["DeviceSubViewModel"]
        DialVM["DialHistoryViewModel"]
    end

    subgraph sg_Persist["持久化抽象层"]
        SqliteMgr["JLDeviceSqliteManager (SDK 单例)"]
        DialCache["BridgeHelper.dialCache() (SDK)"]
        UD["UserDefaults"]
        PlistLoader["PropertyListDecoder"]
    end

    subgraph sg_Storage["存储介质"]
        DB[(SQLite 数据库)]
        FS[(文件系统 / 表盘缓存)]
        UDD[(UserDefaults 域)]
        PlistFile["CodeExample.plist (Bundle)"]
    end

    VC -->|"设备列表回调"| SubVM
    VC -->|"表盘历史/当前表盘"| DialVM
    SubVM -->|"checkout / update"| SqliteMgr
    SubVM -->|"服务器对账后回写"| SqliteMgr
    DialVM -->|"currentWatchName / getWatchList"| DialCache
    SqliteMgr --> DB
    DialCache --> FS
    UD --> UDD
    PlistLoader --> PlistFile
    UD -->|"测试工具 SettingInfo"| PlistLoader

各组件职责:

组件职责数据
JLDeviceSqliteManager(SDK)设备列表的 SQLite 读写,按用户身份隔离UserDeviceModel 列表
BridgeHelper.dialCache()(SDK)表盘本地状态缓存:当前表盘名、已下载表盘列表表盘名称、文件路径
UserDefaults轻量键值持久化WiFi SSID / 密码(测试工具)
PropertyListDecoder解码 Bundle 内 plist 资源[String: String] 配置字典
DeviceSubViewModel编排设备列表的本地读取与服务器同步内存 deviceList + 数据库

其中 JLDeviceSqliteManager、BridgeHelper 的具体实现位于 JL SDK 库中(本仓库不包含其源码,仅包含调用方代码),因此本文对它们的方法签名与行为描述均基于仓库内真实调用点推导,并以「仓库侧证据」标注。

主要实现分析

SQLite 设备数据库:JLDeviceSqliteManager

JLDeviceSqliteManager 是 SDK 暴露的设备数据库管理器,仓库侧通过 JLDeviceSqliteManager.share() 获取单例。从调用点可以归纳出它的核心 API:

  • checkout(by: userIdentify, completion: ([UserDeviceModel]) -> Void):按用户身份标识查询设备列表,结果通过回调返回;
  • update(_ model: UserDeviceModel):插入或更新一条设备记录(以 MAC 等主键为准);
  • update(_ model: UserDeviceModel, time: String):更新设备记录并携带服务器侧更新时间戳,用于后续对账。

关键设计点是按用户隔离:所有查询都携带 userIdentify(来自 User_Http.shareInstance().userPfInfo.identify),保证不同账号的设备列表互不串扰;同时 DeviceSubViewModel 在 init 中先请求用户配置信息(requestGetUserConfigInfo)拿到 identify 后再查询数据库,说明数据库主键/分区依赖用户身份就绪。

func queryDbDevices(){
    JLDeviceSqliteManager.share().checkout(by: userIdentify) { [weak self] list in
        guard let self = self else {return}
        self.deviceList = list
        if Thread.current.isMainThread {
            self.updateListCallBack?(self.deviceList)
        } else {
            DispatchQueue.main.async {
                self.updateListCallBack?(self.deviceList)
            }
        }
    }
}

Source: DeviceSubViewModel.swift

这段代码体现了两个意图:其一,数据库查询结果跨线程安全地回投到主线程(若回调已处于主线程则直接回调,否则 DispatchQueue.main.async),避免 UI 更新线程冲突;其二,queryDbDevices() 是本地数据源与 UI 的桥接点,每次服务器同步完成后都会再次调用它来刷新列表。

设备列表的「本地优先 + 服务器对账」数据流

DeviceSubViewModel 是整个设备持久化流程的编排者。其初始化逻辑如下:

override init() {
    super.init()
    userIdentify = User_Http.shareInstance().userPfInfo.identify
    addNote()

    if userIdentify.count == 0 {
        User_Http.shareInstance().requestGetUserConfigInfo { [weak self] user in
            guard let self = self else {return}
            self.userIdentify = User_Http.shareInstance().userPfInfo.identify
            DispatchQueue.main.async {
                self.queryDbDevices()
                self.queryServerDevices()
            }
        }
        return
    } else {
        queryDbDevices()
    }
    queryServerDevices()
}

Source: DeviceSubViewModel.swift

设计要点:

  • 身份未就绪时串行等待:userIdentify 为空说明用户配置尚未从服务器拉取,此时必须先完成 requestGetUserConfigInfo 才能查询数据库——这是「按用户隔离」的前置约束;
  • 本地先行,网络后补:queryDbDevices()(读 SQLite)总是先于 queryServerDevices()(读服务器)执行,保证 UI 能立刻展示历史设备;
  • 服务器结果回写本地:queryServerDevices 将服务器返回的每条设备通过 update(_:time:) 写回数据库,使本地库成为服务器数据的镜像,供下次冷启动使用:
private func queryServerDevices(){
    DeviceHttp.checkList { resp in
        guard let list = resp else {
            AlertViewOnWindows.getFirstWindow()?.makeToast(LanguageCls.localizableTxt("网络有点问题"),position: .center)
            return
        }
        JLLogManager.logLevel(.DEBUG, content: "DeviceHttp.checkList \(list.count)")
        for item in list {
            JLDeviceSqliteManager.share().update(item.beUdm(), time: item.updateTime)
            item.logProperties()
        }
        self.queryDbDevices()
    }
}

Source: DeviceSubViewModel.swift

这里 item.beUdm() 将服务器响应模型(DeviceHttp 的列表元素)转换为数据库模型 UserDeviceModel,updateTime 用于记录服务器侧时间戳——同步完成后再次调用 queryDbDevices() 使内存 deviceList 与数据库、服务器三方一致。

设备变更事件的本地落库

App 通过 JL_Tools.add("UI_JL_DEVICE_CHANGE", ...) 等通知监听设备状态变化。当用户绑定新设备(通知类型为 2,即「已连接的新设备」)或删除设备时,DeviceSubViewModel 会构建/更新 UserDeviceModel 并立即写库:

@objc private func updateDeviceList(_ note:Notification){
    guard let type = note.object as? NSNumber else {return}
    // 2 为已连接的新设备
    if type.int32Value == 2 {
        guard let (pid,vid) = DialBaseViewModel.shared.getPidVid(),let devModel = BridgeHelper.getCurrentCmdManager()?.outputDeviceModel() ,let currentEntity = BridgeHelper.getCurrentEntity() else { return  }
        let model = UserDeviceModel()
        // ...(构建模型字段,如 mac、uuidStr、产品信息等)
        JLDeviceSqliteManager.share().update(model)
        self.deviceList.removeAll(where: {$0.mac == model.mac})
        // ...(插入并回调 UI)
    }
}

Source: DeviceSubViewModel.swift

同一模式也出现在删除与解绑流程中:服务器确认解绑(或绑定失败)后,DeviceSubViewModel 都会调用 JLDeviceSqliteManager.share().update(model) 更新本地记录,并配合 deviceList.removeAll(where:) 维护内存列表。这说明本地数据库是「事件驱动更新」的——每次设备状态变更都同步落库,避免崩溃/杀进程导致状态丢失。

UserDefaults 轻量持久化(测试工具)

SDKTestHelper 的 SettingInfo 工具类使用系统 UserDefaults 保存测试所需的 WiFi 参数,这是典型的「低价值、小体积」配置存储场景:

class func saveSSID(_ ssid: String) {
    UserDefaults.standard.set(ssid, forKey: "ssid")
    UserDefaults.standard.synchronize()
}

Source: SettingInfo.swift

class func getSSID() -> String? {
    return UserDefaults.standard.string(forKey: "ssid")
}

Source: SettingInfo.swift

savePassword/getPassword 采用相同模式。这里显式调用 synchronize() 属于历史兼容写法(现代 iOS 无需手动同步,系统会自动落盘),但其意图是确保测试工具在进程退出前参数不丢失。

plist 资源配置读取

SourceHelper 展示了 plist 资源的读取模式:通过 R.swift 生成的文件资源引用 R.file.codeExamplePlist.url() 获取 Bundle 内 URL,再用 PropertyListDecoder 解码为 [String: String]:

var codeDict: [String: String] {
    if let url = R.file.codeExamplePlist.url(),
       let data = try? Data(contentsOf: url) {
        let decoder = PropertyListDecoder()
        if let plistData = try? decoder.decode([String: String].self, from: data) {
            return plistData
        }
    }
    return [:]
}

Source: SourceHelper.swift

这段代码体现了「只读资源配置」与「可写数据」的区分:plist 作为随包分发的静态资源(例如测试用例示例),使用只读解码;而运行时产生的状态(表盘选择、设备列表)则写入 SQLite 或缓存。

表盘本地缓存:BridgeHelper.dialCache()

表盘(Dial)相关的本地状态由 BridgeHelper.dialCache() 维护,DialHistoryViewModel 通过 KVO 观察其 currentWatch 属性实现 UI 联动:

observer = BridgeHelper.dialCache().observe(\.currentWatch, changeHandler: { obj, value in
    self.updateCurrentWatch()
})
JL_Tools.add(JL_WATCH_FACE_LIST, action: #selector(updateCurrentWatch), own: self)

Source: DialHistoryViewModel.swift

func setCurrentDial(_ model:DialPayHistoryModel){
    let currentDial = BridgeHelper.dialCache().currentWatchName()
    let dialList = BridgeHelper.dialCache().getWatchList() as? [String] ?? []
    // ... 若本地已有同名表盘,直接切换;否则下载后写入文件系统
    if dialList.contains(model.name.uppercased()) {
        manager.cmdWatchFlashPath("/" + model.name.uppercased(), flag: .setDial) { flag, size, path, describe in
            DispatchQueue.main.async {
                if flag == 0 {
                    BridgeHelper.dialCache().setCurrrentWatchName(model.name.uppercased())
                }
            }
        }
    }
    // else 分支:DialMarketHttp 下载 watch 数据,DialManager.addFile 写入手表文件系统
}

Source: DialHistoryViewModel.swift

表盘缓存是「文件系统 + 内存缓存」的混合持久化:getWatchList() 返回本地已存在表盘名的集合(对应手表 Flash 中已刷入的表盘文件),setCurrrentWatchName 记录当前生效的表盘;只有切换命令成功(flag == 0)才更新缓存,保证缓存状态与设备实际状态一致。

核心流程

设备列表的完整生命周期(冷启动 → 同步 → 变更落库)

sequenceDiagram
    participant App as App (DeviceSubViewModel)
    participant Net as User_Http / DeviceHttp
    participant DB as JLDeviceSqliteManager (SQLite)
    participant UI as 设备列表 UI

    Note over App: 冷启动
    App->>App: init() 读取 userPfInfo.identify
    alt identify 为空
        App->>Net: requestGetUserConfigInfo()
        Net-->>App: identify
    end
    App->>DB: checkout(by: identify)
    DB-->>App: 本地设备列表 [UserDeviceModel]
    App->>UI: updateListCallBack (主线程)
    App->>Net: queryServerDevices() → DeviceHttp.checkList
    Net-->>App: 服务器设备列表
    loop 每条设备
        App->>DB: update(beUdm(), time: updateTime)
    end
    App->>DB: checkout(by: identify) 重新查询
    DB-->>App: 最新列表
    App->>UI: 刷新列表
    Note over App,DB: 运行期事件
    Net-->>App: UI_JL_DEVICE_CHANGE / 解绑回调
    App->>DB: update(model) 事件驱动落库

流程要点:

  1. 身份就绪是数据库访问的前置条件:数据库按 identify 分区,身份未知时先请求用户配置;
  2. 读本地 → 显示 → 同步 → 回写:本地数据先行渲染保证启动速度,服务器数据随后对账;
  3. 事件驱动落库:绑定/解绑/设备变更通过通知或回调触发 update(model),保证数据库始终反映最新状态;
  4. 主线程安全:数据库回调统一切换回主线程后才驱动 UI。

表盘切换的本地状态流

sequenceDiagram
    participant U as 用户
    participant VM as DialHistoryViewModel
    participant C as BridgeHelper.dialCache()
    participant M as mFlashManager (设备)

    U->>VM: 选择表盘 setCurrentDial(model)
    VM->>C: currentWatchName() / getWatchList()
    C-->>VM: 当前表盘 / 本地表盘列表
    alt 本地已存在该表盘
        VM->>M: cmdWatchFlashPath(setDial)
        M-->>VM: flag == 0 (成功)
        VM->>C: setCurrrentWatchName(name)
    else 本地不存在
        VM->>VM: DialMarketHttp 下载表盘数据
        VM->>VM: DialManager.addFile 写入设备
        VM->>VM: updateStatus 回调进度
    end
    Note over VM,C: 成功后才更新缓存,保持缓存与设备一致

数据模型与持久化映射

UserDeviceModel 与 SQLite 的映射

UserDeviceModel 是设备列表在内存与数据库之间的统一模型:

  • 服务器响应模型通过 beUdm() 转换为 UserDeviceModel 后写入数据库;
  • 数据库查询结果(checkout 回调)直接以 [UserDeviceModel] 返回给 ViewModel;
  • mac 字段作为内存列表去重主键(deviceList.removeAll(where: {$0.mac == model.mac})),推测也是数据库的唯一键;
  • 每条记录伴随 updateTime(服务器侧时间戳),支撑同步对账。
erDiagram
    USER_DEVICE {
        string mac PK "设备唯一标识"
        string user_identify "按用户分区"
        string uuid_str "UUID"
        string update_time "服务器时间戳"
        string explain "设备说明"
    }
    USER_CONFIG {
        string identify PK "用户身份标识"
    }
    USER_DEVICE }o--|| USER_CONFIG : "按 identify 隔离"

说明:UserDeviceModel 的完整字段定义位于 JL SDK 库中(本仓库不含其源码),上表仅列出从仓库调用点可确证的字段(mac、uuidStr、updateTime、explain、userID),其余字段请以 SDK 头文件为准。

键值存储映射(UserDefaults)

Key类型读写方用途
ssidStringSettingInfo.saveSSID/getSSID测试 WiFi 名称
passwordStringSettingInfo.savePassword/getPassword测试 WiFi 密码
currentWatch(KVO 可观察)BridgeHelper.dialCache当前生效表盘

静态资源映射(plist)

资源解码目标使用方用途
CodeExample.plist[String: String]SourceHelper.codeDict测试代码示例配置

API Reference(仓库侧调用点归纳)

以下签名均基于仓库内真实调用点归纳。JLDeviceSqliteManager 与 BridgeHelper.dialCache() 的完整定义位于 JL SDK 库中,此处仅列出仓库侧可确证的使用契约。

JLDeviceSqliteManager.share() -> JLDeviceSqliteManager

获取设备数据库管理器的全局单例。仓库中所有数据库读写都通过该单例进行,保证数据库连接与事务的全局唯一性。

调用示例(见 DeviceSubViewModel.swift):

JLDeviceSqliteManager.share().checkout(by: userIdentify) { list in ... }

checkout(by: String, completion: ([UserDeviceModel]) -> Void)

按用户身份标识查询设备列表。

  • by(String):用户身份标识 identify,来自 User_Http.shareInstance().userPfInfo.identify;
  • completion:查询结果回调,参数为设备模型数组。回调可能不在主线程,UI 更新前需自行切换线程。

update(_ model: UserDeviceModel)

插入或更新一条设备记录(以 mac 等唯一键为准),用于绑定新设备、解绑、设备信息变更等事件驱动场景。

update(_ model: UserDeviceModel, time: String)

更新设备记录并记录服务器侧时间戳 updateTime,用于服务器列表同步回写(见 queryServerDevices)。

BridgeHelper.dialCache() 相关方法

方法行为
currentWatchName() -> String返回当前生效的表盘名称(与设备实际状态一致)
getWatchList() -> [Any]?返回本地已下载/已刷入的表盘名称列表
setCurrrentWatchName(_ name: String)记录当前表盘名称,仅在设备切换命令成功(flag == 0)后调用
currentWatch(属性)可通过 KVO 观察,DialHistoryViewModel 用它驱动 UI 刷新

SettingInfo(测试工具)

方法说明
saveSSID(_ ssid: String)写入 UserDefaults 的 ssid 键
getSSID() -> String?读取 ssid 键
savePassword(_ password: String)写入 password 键
getPassword() -> String?读取 password 键

配置选项

配置项类型默认值说明
ssid(UserDefaults)String无测试 WiFi 名称,由 SettingInfo 管理
password(UserDefaults)String无测试 WiFi 密码
CodeExample.plist资源文件随包分发SourceHelper 只读解码的静态配置
userIdentify(内存)String""数据库查询分区键,从 userPfInfo.identify 获取;为空时先请求用户配置

故障模式、边界情况与并发

身份标识未就绪

init 中 userIdentify 为空(用户配置尚未拉取)时,直接查询数据库会得到错误分区甚至空结果。DeviceSubViewModel 的处理是:先调用 requestGetUserConfigInfo 阻塞式获取 identify,成功后才在 DispatchQueue.main.async 中执行数据库查询与服务器同步。若该请求失败,设备列表将保持为空,reconnectLast 等依赖列表的功能也会退化(见 DeviceSubViewModel.swift)。

网络失败与本地回退

queryServerDevices 中 DeviceHttp.checkList 返回 nil 时(网络问题),仅提示 Toast 并放弃同步,不清理本地数据库。这是刻意的设计:本地 SQLite 作为离线兜底,网络失败时用户仍能看到上次同步的设备列表,且 reconnectLast 仍可基于本地首条设备发起连接。

服务器绑定冲突

当服务器返回「设备已被其他人绑定」时,代码路径会先 update(model) 更新本地状态再提示警告(见 DeviceSubViewModel.swift),说明绑定失败也会以「更新本地记录」的方式落库,避免本地与服务器状态长期不一致。

线程与并发

  • 数据库回调线程不定:checkout 的回调可能在后台线程执行,queryDbDevices 显式判断 Thread.current.isMainThread 后统一切回主线程再触发 updateListCallBack(L42-L48);
  • UI 主线程约束:表盘切换、下载进度等 UI 更新均包裹在 DispatchQueue.main.async 中;
  • 通知驱动更新:UI_JL_DEVICE_CHANGE、JL_BATTERY、UI_DELETE_DEVICE_MODEL、kJL_BLE_M_ON 等多个通知可能并发触发设备列表更新,DeviceSubViewModel 通过 deviceList 单一数据源 + 回调去重来收敛竞态(内存列表以 mac 去重)。

边界情况:表盘缓存与设备状态一致性

setCurrentDial 先比较 model.name.uppercased() 与 currentDial,相同则直接返回(幂等保护);只有设备端切换成功(flag == 0)才更新缓存 setCurrrentWatchName,避免缓存先行导致 UI 与设备实际表盘不一致。若下载失败(data == nil),提示「下载失败」且不触碰缓存。

性能与运维注意

  • 本地优先降低启动延迟:冷启动直接读 SQLite 渲染设备列表,避免首屏等待网络;服务器同步异步进行,不阻塞 UI;
  • 全量回写策略:queryServerDevices 对服务器列表逐条 update(_:time:),属于「读改写」模式——设备量级不大(个位数到几十台)时开销可忽略,若未来设备量增大可考虑批量事务;
  • UserDefaults.synchronize():SettingInfo 中的显式同步属于历史 API,现代系统会自动落盘;测试工具场景下无性能影响;
  • plist 只读缓存:SourceHelper.codeDict 每次访问都重新解码文件,若频繁调用可考虑缓存解码结果(当前为测试工具,频率低,无优化必要)。

扩展点

  • 新增设备字段持久化:扩展 UserDeviceModel(SDK 侧)并沿用 update(model) 即可,仓库侧无需改动调用点;
  • 多账号切换:checkout(by:) 已按 identify 分区,切换账号只需更换 userIdentify 后重新 queryDbDevices();
  • 表盘缓存联动:DialHistoryViewModel 通过 KVO 观察 dialCache().currentWatch,新增表盘状态维度时可在 dialCache 上增加可观察属性并沿用同一观察模式;
  • 测试参数扩展:SettingInfo 中每新增一个键值对只需复制 save/get 模式,无表结构迁移成本。

测试情况

  • code/JL_Health/JieliJianKang/UnitTester/DataTester.swift 存在数据层测试器(本页证据收集预算内未读取其内容,建议查阅以了解数据库/数据模型的单元测试用例);
  • SDKTestHelper 工程中的 SettingInfo、SourceHelper 为测试工具代码,其 UserDefaults 与 plist 读取逻辑可被测试工程直接复用;
  • 设备列表的持久化行为主要通过 DeviceSubViewModel 与 SDK 的集成验证(真机绑定/解绑后重启 App 检查列表)。

Related Links

  • DeviceSubViewModel.swift(设备列表持久化编排)
  • DialHistoryViewModel.swift(表盘缓存使用)
  • SettingInfo.swift(UserDefaults 测试工具)
  • SourceHelper.swift(plist 资源读取)
  • DataTester.swift(数据层测试器)
  • 相关页面:表盘市场(Dial Market)、网络层与服务器同步、蓝牙连接管理(SDK 侧)
Prev
AI 云服务与语音交互
Next
多语言国际化