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

    • 项目概述与功能特性
    • 工程结构与运行环境
  • 快速开始

    • SDK 集成步骤
    • 连接方式选择指南
  • 核心 SDK 架构

    • SDK 框架组成
    • JL_OTAManager 升级管理 API
    • 设备认证与广播解析
  • 蓝牙连接与设备发现

    • 设备扫描与广播发现
    • 原生 CoreBluetooth 连接
    • JL_BLEKit SDK 连接
    • JL_Assist 自定义连接
    • GATT Over BR/EDR 经典蓝牙升级
  • OTA 升级工作流

    • 标准升级流程
    • 自动化测试与批量升级
    • 广播音箱升级
    • 升级文件管理
  • 示例工程

    • 完整示例应用
    • 迷你示例工程
    • 第三方依赖与工具
  • 开发支持与版本发布

    • 文档中心与 API 说明
    • SDK 版本与构建产物
    • 调试技巧与日志辅助

调试技巧与日志辅助

本文档介绍 iOS-JL_OTA SDK 的日志辅助体系(JLLogHelper.xcframework 与 JLLogManager API)以及针对蓝牙 OTA 升级场景的调试技巧,帮助开发者定位连接、认证、传输过程中的问题。

Purpose and Scope

本页聚焦于 OTA 开发中的可观测性能力,覆盖:

  • JLLogHelper.xcframework(日志打印收集库)的集成与角色定位;
  • JLLogManager 的全部公开接口(Objective-C 与 Swift 双语法);
  • 日志开关、级别、时间戳、文件存储、路径重定向与收集回调等配置;
  • 基于 Xcode Console 与 SDK 日志的调试排查流程。

本页不涉及 OTA 升级业务流程本身(如 cmdOTAData、otaUpgradeResult 回调的时序)——相关内容请参见「OTA 升级开发示例」系列页面(code/MiniDemo/ 下三种连接方式的示例文档);蓝牙连接的实现细节请参见「蓝牙连接」相关页面。由于 SDK 以预编译 XCFramework 形式分发,本页对实现机制的分析基于仓库内 README 与示例工程文档中公开的接口契约,二进制内部实现细节不在仓库中可见,文中已如实标注。

Overview

为什么需要日志辅助

杰理 OTA 升级基于 RCSP 协议,运行在低功耗蓝牙(BLE)之上。BLE 通信具有异步、低吞吐、易中断的特点,一次升级包含设备发现 → 连接 → 认证(HashPair)→ 广播包解析(AdvParse)→ 特征订阅 → 固件分包下发 → 结果确认等多个阶段,任何一环失败都会导致升级中断。这些阶段彼此依赖、时序性强,仅靠 UI 层的错误提示难以定位根因,因此 SDK 提供了独立的 JLLogHelper 日志库,将底层各模块(蓝牙状态、数据交互、分包进度)的日志统一输出、收集与落盘,形成完整的排查链路。

关键设计意图

JLLogHelper 与业务库 JL_OTALib 解耦,作为独立的 XCFramework 存在,其设计意图包括:

  1. 默认开启、开箱即用:集成后无需任何初始化代码即可获得打印 + 文件存储双重日志,降低接入门槛;
  2. 可裁剪:生产环境可通过一行 API 关闭打印或存储,避免日志文件无限增长与性能损耗;
  3. 可接管:collectLog 回调允许把 SDK 日志转发到第三方日志系统(如 CocoaLumberjack、自建上报平台),实现统一可观测性;
  4. 可重定向:redirectLogPath 支持把日志文件写入指定沙盒路径,便于按 App 自身的文件管理策略组织日志。

核心概念

概念说明
JLLogManager日志辅助库的唯一入口类,提供开关、级别、存储、收集等静态方法
日志级别控制日志详细程度,如 JLLOG_COMPLETE(完整日志)
打印(Print)日志输出到 Xcode Console,供开发期实时观察
存储(File)日志异步写入文件,供事后导出分析
收集(Collect)通过回调把每一条日志实时转发给调用方

Architecture

flowchart TD
    subgraph sg_App["App 宿主工程"]
        App["业务代码 / UI"]
        BLE["BLE 连接层<br/>(CoreBluetooth / JL_BLEKit / JL_Assist)"]
    end

    subgraph sg_SDK["iOS-JL_OTA SDK (XCFramework)"]
        OTA["JL_OTALib<br/>OTA 升级业务库"]
        ADV["JL_AdvParse<br/>广播包解析库"]
        HASH["JL_HashPair<br/>设备认证库"]
        LOG["JLLogHelper<br/>(JLLogManager)"]
    end

    subgraph sg_Sink["日志出口"]
        Console["Xcode Console"]
        File["沙盒日志文件"]
        Callback["collectLog 回调<br/>→ 第三方日志平台"]
    end

    App --> BLE
    BLE --> OTA
    OTA --> ADV
    OTA --> HASH
    OTA --> LOG
    BLE --> LOG
    ADV --> LOG
    HASH --> LOG
    LOG -->|"打印 (setLog)"| Console
    LOG -->|"存储 (saveLogAsFile)"| File
    LOG -->|"收集 (collectLog)"| Callback
    File -->|"redirectLogPath 可重定向"| Callback

架构说明:JLLogHelper 处于 SDK 各业务库(OTA、AdvParse、HashPair)以及蓝牙连接层之下游,统一承接它们的日志输出。日志的三种出口(Console / 文件 / 回调)相互独立、可分别开关,其中文件出口的路径可通过 redirectLogPath 重定向,回调出口可用于接入外部日志平台。这种「收集与消费解耦」的结构使调试能力可以在不修改业务代码的前提下按环境(开发/生产)灵活裁剪。

核心实现详解

JLLogManager:日志辅助库的单一入口

JLLogHelper.xcframework 对外暴露的核心类是 JLLogManager,全部接口为静态方法,无需实例化,集成后立即可用。仓库中公开的接口契约如下(Objective-C 与 Swift 双语法等价):

能力Objective-CSwift
开关打印setLog:IsMore:Level:setLog(_:isMore:level:)
开关文件存储saveLogAsFile:saveLog(asFile:)
开关时间戳logWithTimestamp:log(withTimestamp:)
重定向存储路径—redirectLogPath(_:)
清空日志clearLogclearLog()
收集日志回调—collectLog { str in ... }
主动写入一条日志—logSomething(_:)

说明:redirectLogPath、collectLog、logSomething 在仓库 README 中仅给出 Swift 示例,其 Objective-C 等价接口以 SDK 头文件为准(二进制库内实现不可见,故本页仅依据仓库可见文档如实记载)。

日志级别控制

日志级别通过 JLLOG_COMPLETE 这类枚举值传入 setLog:IsMore:Level:,用于权衡信息量与性能:

  • 开发阶段建议使用完整级别(JLLOG_COMPLETE),配合 IsMore:true 输出更多调试细节;
  • 发布阶段建议降低级别或直接关闭打印,仅保留文件存储用于线上问题回溯。

IsMore 参数控制是否输出更详细的辅助信息(如数据帧内容、时序细节),默认情况下两者结合使用可获得最完整的 BLE 交互轨迹。

默认行为与开关语义

根据 README.md 的说明,JLLogHelper.framework 默认开启了日志打印和存储,即集成后立即生效。三个开关的语义:

  • setLog:false —— 关闭控制台打印(SDK 内部仍可记录,只是不输出);
  • saveLogAsFile:false —— 关闭文件存储(避免生产环境日志文件膨胀);
  • logWithTimestamp:false —— 关闭日志行首的时间戳(减少冗余、便于对齐自有日志格式)。

关闭打印并不影响文件存储,反之亦然——三个维度彼此独立,可按需组合。

日志存储与路径管理

日志默认写入 App 沙盒内 JLLogHelper 自管理的位置。Swift 侧提供了路径重定向能力:

let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + "/abc.txt"
JLLogManager.redirectLogPath(path) // 重置保存路径

设计意图:SDK 不强制日志存放位置,而是把「日志文件属于 App 的 Documents 目录」这一决策交给宿主应用。App 可以借此把 SDK 日志与自有日志合并管理、统一导出或上传,也便于在 App 内做「一键导出日志」功能。

日志收集回调(接入第三方日志系统)

collectLog 以回调形式把 SDK 内部产生的每一条日志实时转发给宿主:

JLLogManager.collectLog { str in
    print(str) // 回调所有的日志打印内容
}

该回调是 JLLogHelper 与外部可观测性体系对接的扩展点:宿主只需在回调内把 str 转发给 CocoaLumberjack、Sentry、自建上报 API 等,即可让 SDK 日志融入既有日志链路,无需改动 SDK 内部实现。注意回调是在 SDK 日志产生的线程上触发的,若需跨线程转发,应自行做线程切换。

主动打点与清空

JLLogManager.logSomething("abcd") 允许宿主主动写入一条自定义日志,使 SDK 日志流中可插入业务上下文标记(例如「开始升级 2.5.0」「用户取消」),便于对时间线;clearLog 用于清空已收集/已存储的日志,适合在「开始新一轮升级」前重置现场,避免多次升级日志混淆。

Core Flow

日志产生 → 出口分发的完整链路

sequenceDiagram
    participant SDK as SDK 业务模块<br/>(OTALib/AdvParse/HashPair/BLE)
    participant M as JLLogManager
    participant C as Xcode Console
    participant F as 沙盒日志文件
    participant H as 宿主 App (collectLog 回调)

    Note over SDK,H: 初始化:默认打印+存储已开启
    H->>M: setLog(true, isMore:true, level:.COMPLETE)
    H->>M: saveLog(asFile:true)
    H->>M: redirectLogPath(Documents/abc.txt)
    H->>M: collectLog { str in 转发到外部平台 }

    loop 升级过程中每个事件
        SDK->>M: 输出日志 (蓝牙状态/数据交互/分包进度)
        M->>C: 控制台打印
        M->>F: 异步写入文件
        M->>H: 回调转发 str
    end

    H->>M: clearLog()  // 新一轮升级前重置现场
    H->>M: saveLog(asFile:false)  // 生产环境关闭存储
    Note over H: 事故现场日志已导出/上传,分析完毕

流程说明:整个链路中 JLLogManager 是唯一枢纽。SDK 各业务模块产生日志后,JLLogManager 依据三个独立开关分别分发到控制台、文件与回调;宿主在升级前完成配置(级别、路径、回调),升级中被动接收日志,升级后通过 clearLog 清理现场。这种「配置前置、消费后置」的模式保证了日志采集对升级主流程零侵入。

问题排查工作流

flowchart TD
    Start([升级失败或行为异常]) --> S1["开启完整日志<br/>setLog(true, isMore:true, level:.COMPLETE)"]
    S1 --> S2{"Console 实时观察<br/>是否可复现?"}
    S2 -->|"可复现"| S3["Xcode Console 过滤<br/>查看蓝牙状态与数据交互日志"]
    S2 -->|"不可复现/用户现场"| S4["导出沙盒日志文件<br/>(saveLogAsFile:true)"]
    S3 --> S5{"定位到阶段?"}
    S4 --> S5
    S5 -->|"连接/广播阶段"| D1["检查 AdvParse 广播解析<br/>与 BLE 连接日志"]
    S5 -->|"认证阶段"| D2["检查 HashPair 认证日志"]
    S5 -->|"传输阶段"| D3["检查 cmdOTAData 分包<br/>与 otaDataSend 进度日志"]
    D1 --> R["对照官方 SDK 调试说明<br/>doc.zh-jieli.com/.../debug.html"]
    D2 --> R
    D3 --> R
    R --> End([定位根因并修复])

使用示例

以下示例均摘自仓库 README.md 的「5.4 日志管理」章节,是官方给出的唯一权威用法。

Objective-C:开关控制

// Objective-C
[JLLogManager clearLog]; // 清空日志
[JLLogManager setLog:false IsMore:false Level:JLLOG_COMPLETE]; // 关闭日志打印
[JLLogManager saveLogAsFile:false]; // 关闭日志存储
[JLLogManager logWithTimestamp:false]; // 关闭日志打印时间

Source: README.md

该示例展示生产环境的典型裁剪组合:关闭打印与时间戳、按需关闭存储,仅保留最低开销的运行状态。

Swift:完整能力演示

// Swift
JLLogManager.saveLog(asFile: true)
JLLogManager.setLog(true, isMore: false, level: .COMPLETE)
JLLogManager.log(withTimestamp: true)
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + "/abc.txt"
JLLogManager.redirectLogPath(path) // 重置保存路径
JLLogManager.clearLog()
JLLogManager.collectLog { str in
    print(str) // 回调所有的日志打印内容
}
JLLogManager.logSomething("abcd")

Source: README.md

该示例覆盖了本页讨论的全部能力:存储开关、级别/冗余控制、时间戳开关、路径重定向、清空、收集回调与主动打点。Swift 语法中枚举值写作 .COMPLETE(对应 ObjC 的 JLLOG_COMPLETE)。

与 OTA 主流程的组合用法

// 1. 升级前:重置现场并开启完整日志
[JLLogManager clearLog];
[JLLogManager setLog:true IsMore:true Level:JLLOG_COMPLETE];
[JLLogManager saveLogAsFile:true];

// 2. 正常走 OTA 流程:noteEntityConnected → cmdTargetFeature → cmdOTAData(data)
//    期间 JLLogManager 自动记录蓝牙状态与数据交互

// 3. 升级结束:如需导出日志,日志文件已在沙盒内;随后清空现场
[JLLogManager clearLog];

Source: README.md(OTA 主流程调用顺序)、README.md(日志接口)

配置选项

配置项(API)类型默认值说明
setLog(_:isMore:level:) / setLog:IsMore:Level:Bool × 2 + Level 枚举打印开启控制控制台打印开关、是否输出更多细节、日志详细级别
saveLog(asFile:) / saveLogAsFile:Booltrue(开启存储)控制日志是否异步写入沙盒文件
log(withTimestamp:) / logWithTimestamp:Bool开启控制日志行首是否附带时间戳
redirectLogPath(_:)String(路径)SDK 默认沙盒位置重置日志文件保存路径
collectLog(completion:)回调 (String) -> Void无(不收集)每产生一条日志即回调一次,可转发到第三方日志平台
logSomething(_:)String—主动向 SDK 日志流写入一条自定义日志
clearLog() / clearLog——清空已收集/已存储的日志

环境建议:

  • 开发/联调:setLog(true, isMore: true, level: .COMPLETE) + saveLog(asFile: true),在 Xcode Console 实时观察;
  • 生产发布:setLog(false, isMore: false, level: .COMPLETE)、按需 saveLog(asFile: false),必要时保留 collectLog 做线上采样上报。

API Reference

以下为 JLLogManager 在仓库文档中公开的接口(Swift 为主,ObjC 等价签名标注在括号内)。

setLog(_ enabled: Bool, isMore: Bool, level: JLLogLevel) / setLog:IsMore:Level:

控制控制台打印输出。

参数:

  • enabled(Bool):是否输出日志到控制台。false 时静默 SDK 打印,但文件存储与收集回调不受影响;
  • isMore(Bool):是否输出更多细节(数据帧内容、时序等)。开发期建议 true;
  • level(JLLogLevel,如 JLLOG_COMPLETE / .COMPLETE):日志详细级别,级别越高信息越全、开销越大。

返回值: 无。

saveLog(asFile: Bool) / saveLogAsFile:

控制日志是否写入文件。

参数:

  • asFile(Bool):true 开启文件存储(默认),false 关闭。生产环境关闭可避免日志文件持续增长。

返回值: 无。

log(withTimestamp: Bool) / logWithTimestamp:

控制日志行首时间戳。

参数:

  • withTimestamp(Bool):true 输出时间戳(默认),false 关闭。

返回值: 无。

redirectLogPath(_ path: String)

重置日志文件保存路径。

参数:

  • path(String):目标文件完整路径,示例中使用 Documents 目录下的 abc.txt。

返回值: 无。

注意: 应在产生大量日志前调用;运行时切换路径可能导致已打开文件句柄的日志写入新路径的行为变化,仓库文档未给出切换时机的明确约定,建议在升级流程开始前配置。

clearLog() / clearLog

清空日志。适合在新一轮升级前重置现场,避免多次升级日志混淆。

返回值: 无。

collectLog(completion: (String) -> Void)

注册日志收集回调,SDK 每产生一条日志即调用一次。

参数:

  • completion((String) -> Void):接收完整日志行文本。可在其中转发到第三方日志平台。

返回值: 无。

注意: 回调在 SDK 日志产生线程触发,跨线程转发需自行切换线程上下文。

logSomething(_ content: String)

主动向 SDK 日志流写入一条自定义日志,用于在时间线中插入业务上下文标记。

参数:

  • content(String):要写入的日志内容,如 "abcd"。

返回值: 无。

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

日志文件无限增长

saveLogAsFile 默认开启且 README 未披露自动轮转/清理策略。长时间运行或频繁升级的 App 若不加管控,日志文件会持续膨胀,最终占用沙盒空间。

对策:生产环境关闭文件存储,或定期 clearLog,或在 redirectLogPath 指向的目录上自行实现保留策略(如按日期分文件、保留最近 N 份)。

打印与存储的独立性

三个开关(打印、存储、时间戳)相互独立。若仅关闭打印而忘记关闭存储,生产包仍会持续写文件;反之若仅关闭存储,Console 打印仍会产生 IO 开销(在真机调试时尤其明显)。裁剪时需显式逐项配置,避免「只关了一半」。

收集回调的线程与顺序

collectLog 回调可能在高频日志(如分包传输阶段逐包打点)下被密集触发,且触发线程不固定。若回调内直接执行重 IO(写数据库、同步网络请求),可能反压 SDK 日志链路、造成卡顿。

对策:回调内只做轻量转发(如追加到内存缓冲或异步队列),把真正的持久化/上报放到后台线程;如需严格按时间顺序落库,应在接收侧自行排序或打时间戳。

路径重定向的失败场景

redirectLogPath 指向不存在的目录、无写入权限的路径或非法文件名时,日志写入可能静默失败(仓库文档未说明错误回调)。

对策:重定向前先 FileManager 创建目标目录并验证可写性;由于失败无显式报错,可在 collectLog 侧做旁路校验,确认文件确有内容增长。

二进制库的「黑盒」限制

JLLogHelper 以预编译 XCFramework 分发,仓库内不可见其内部实现。因此日志缓冲策略、文件编码、崩溃时是否强制落盘等细节均无源码依据;排查到此类深水区问题时,应携带 JLLOG_COMPLETE 级别日志联系杰理官方(见社区与支持链接)。

与升级流程的并发关系

日志采集与 OTA 分包传输并发执行。JLLogManager 设计上不阻塞业务线程(打印与写文件均为 SDK 内部异步处理),但极端情况下(日志量极大 + 设备写入慢)日志 IO 仍可能与 BLE 写入竞争资源;如遇升级性能异常,可先关闭 isMore 细节输出,观察是否与日志量相关。

性能与运维注意事项

  • 开发期保持完整日志:setLog(true, isMore: true, level: .COMPLETE) 是排查 BLE 时序问题(连接状态、广播包解析、认证握手、分包进度)的首选手段,官方调试说明入口见 SDK 调试说明。
  • 发布期最小化开销:关闭 isMore 细节输出与 Console 打印可显著降低高频日志(逐包打点)对 CPU/IO 的影响;线上问题回溯依赖文件存储或收集回调,需在发布前确认这两条出口的保留策略。
  • 日志导出节奏:升级失败后尽快导出日志文件(或通过 collectLog 上报),避免后续操作覆盖现场;多轮升级共享同一日志流时,每轮开始前 clearLog 可保持现场干净。
  • 日志与升级性能的权衡:日志级别与 isMore 直接决定日志量;在弱信号、低吞吐的 BLE 链路上,若升级出现超时,优先降低日志冗余度再复测,以排除日志 IO 干扰。

扩展点

JLLogHelper 为宿主预留了两个稳定的扩展通道:

  1. collectLog 回调 → 任意日志平台:SDK 日志以字符串流形式实时交付,宿主可在回调内桥接 CocoaLumberjack、Sentry、自建上报 API 或本地缓冲,无需改动 SDK。这是把 SDK 可观测性纳入公司统一日志体系的推荐做法。
  2. redirectLogPath → 自定义存储策略:日志文件路径由宿主决定,可据此实现按日期分目录、多文件轮转、与 App 自有日志合并导出等策略。注意重定向应在升级流程开始前完成。

除此之外,JLLogHelper 不暴露日志格式定制(如自定义 tag、过滤规则)等接口;此类需求可通过 collectLog 在接收侧二次加工实现。

相关链接

  • README.md(日志管理 5.4 与调试技巧 6) —— 本页全部接口与调试说明的权威出处
  • README_EN.md —— 英文版 README(含 Debugging Tips 章节)
  • OTA 升级开发示例.md(MiniSingleDemo,原生 CoreBluetooth 连接) —— 与日志辅助配套的完整升级流程示例
  • OTA升级开发示例(SDK蓝牙连接).md(JLBleKitOTADemo) —— JL_BLEKit 连接方式下的升级示例
  • OTA 升级开发示例(JL_Assist 自定义蓝牙连接).md(JLAssistOTADemo) —— JL_Assist 连接方式下的升级示例
  • doc/API 说明.md —— SDK API 说明文档
  • 官方在线文档中心:https://doc.zh-jieli.com/(调试说明:Apps/iOS/ota/zh-cn/master/Other/debug.html)
Prev
SDK 版本与构建产物