JLLogHelper 日志工具
JLLogHelper 是杰理科技(Jieli)iOS 蓝牙 SDK 系列中的统一日志辅助框架,以 JLLogManager 单例式类方法 API 为核心,提供带等级过滤、函数名/行号定位、时间戳、文件保存、路径重定向与日志收集回调等能力的轻量级日志打印解决方案。
Purpose and Scope
本页介绍 JLLogHelper.xcframework 的完整能力:
JLLogManager核心类的全部类方法 API 与设计意图;kJLLog打印宏与JLLOG_LEVEL日志等级的定义与语义;- 日志从调用点到控制台/文件/回调的完整数据流;
- 配置项、失败模式、并发与性能考量、扩展方式。
相关但属于其他目录页的主题不在本页展开:音频处理框架请参见 JLAudioUnitKit 相关页面;图像转换请参见 JLBmpConvertKit;资源包处理请参见 JLPackageResKit。本页只聚焦日志框架本身的机制与用法。
Overview
JLLogHelper 是整个 iOS-JL_Bluetooth 仓库中被 JieLi_Home_Demo 等示例工程统一引用的日志基础库(见 JieLiAppHeader.pch 中的 #import <JLLogHelper/JLLogHelper.h>)。其设计目标可以概括为三点:
- 零成本接入:业务代码只需
#import <JLLogHelper/JLLogHelper.h>,即可通过kJLLog宏或JLLogManager类方法打印日志,无需额外初始化。 - 可分级、可定位:日志分五级(
JLLOG_COMPLETE到JLLOG_ERROR),可整体使能/禁用,并可选择在输出中附带函数名 + 行号,便于在蓝牙协议栈这类回调密集的代码中快速定位问题。 - 可持久化、可采集:支持把日志保存为文件、重定向保存路径、按时间戳打印,以及通过 block 回调把日志实时转发给上层(例如用于 SDK 联调抓包或上传崩溃现场)。
框架以静态库 + 头文件的方式打包为 JLLogHelper.xcframework,支持 iOS(arm64)、iOS 模拟器(arm64 + x86_64)与 macOS(arm64 + x86_64)三种平台切片,由 libs/JLLogHelper.xcframework/Info.plist 声明。
Architecture
flowchart TD
subgraph sg_App["应用层 (App Code)"]
Macro["kJLLog 打印宏<br/>自动携带 __FUNCTION__ / __LINE__"]
Direct["JLLogManager 类方法调用<br/>logSomething / logObject / logLevel:content:"]
end
subgraph sg_Framework["JLLogHelper.framework"]
Umbrella["JLLogHelper.h<br/>入口头文件 + 版本符号"]
Manager["JLLogManager<br/>日志核心类(类方法)"]
Level["JLLOG_LEVEL 枚举<br/>COMPLETE~ERROR"]
end
subgraph sg_Output["日志输出目标"]
Console["控制台打印"]
LogFile["日志文件存储<br/>saveLog / openLogTextFile"]
Callback["collectLog block 回调"]
end
Macro -->|"展开为 logLevel:funcName:line:format:"| Manager
Direct --> Manager
Umbrella --> Manager
Level --> Manager
Manager --> Console
Manager --> LogFile
Manager --> Callback
架构要点说明:
- 入口收敛:
JLLogHelper.h是框架唯一的伞头文件(umbrella header),对外只暴露JLLogManager.h,同时导出JLLogHelperVersionNumber与JLLogHelperVersionString两个版本符号,供上层判断框架版本。 - 类方法设计:
JLLogManager的所有能力均为+类方法,内部以单例/静态状态持有配置(使能开关、等级、时间戳、文件保存开关、保存路径、收集回调等),因此不需要实例化,天然全局唯一,符合日志组件的定位。 - 宏是唯一推荐入口:
kJLLog宏借助编译器内置的__FUNCTION__与__LINE__自动注入调用位置,是setLog:IsMore:中"是否打印函数名与行号"能力的实现基础;直接调用类方法也可以,但需要手动传函数名和行号。
核心实现解析
日志等级枚举 JLLOG_LEVEL
等级定义见 JLLogManager.h:
typedef NS_ENUM(NSInteger, JLLOG_LEVEL) {
JLLOG_COMPLETE = 0,
JLLOG_DEBUG = 1,
JLLOG_INFO = 2,
JLLOG_WARN = 3,
JLLOG_ERROR = 4,
};
设计语义:
| 等级 | 值 | 用途 |
|---|---|---|
JLLOG_COMPLETE | 0 | 全量日志(不过滤),适合协议抓包、原始数据流跟踪 |
JLLOG_DEBUG | 1 | 调试信息,框架默认等级 |
JLLOG_INFO | 2 | 常规运行信息 |
JLLOG_WARN | 3 | 警告,不阻塞流程但需关注 |
JLLOG_ERROR | 4 | 错误,通常伴随异常路径或失败返回 |
等级值从小到大对应"严重程度"递增、日志量递减。setLog:IsMore:Level: 设置的等级作为过滤阈值:只有 level >= 阈值 的日志才会被输出,JLLOG_COMPLETE 因此是"输出一切"的特殊档位。
核心数据流与过滤机制
kJLLog 宏的展开
宏定义见 JLLogManager.h:
#define kJLLog(level,fmt...) [JLLogManager logLevel:level funcName:__FUNCTION__ line:__LINE__ format:fmt]
这是典型的可变参数转发宏:fmt... 原样透传给 logLevel:funcName:line:format:... 的可变参数,__FUNCTION__ 和 __LINE__ 在宏展开处求值,因此无论日志代码位于哪个类、哪一行,输出都能精确指向调用点。这也意味着宏只能用于 Objective-C 函数/方法上下文;Swift 侧建议直接调用 logLevel:content: 或 logSomething: 类方法。
日志过滤与输出流程
flowchart TD
Start(["调用 kJLLog / logLevel"]) --> CheckEnable{"enable == YES?"}
CheckEnable -->|"NO"| Drop1(["直接丢弃,零开销返回"])
CheckEnable -->|"YES"| CheckLevel{"传入 level >= 设定阈值?"}
CheckLevel -->|"NO"| Drop2(["低于阈值,丢弃"])
CheckLevel -->|"YES"| Build["拼接前缀<br/>时间戳(可选) + 函数名/行号(可选)"]
Build --> Console["控制台打印"]
Console --> IsSave{"saveLogAsFile == YES?"}
IsSave -->|"YES"| File["写入日志文件<br/>(redirectLogPath 指定目录)"]
IsSave -->|"NO"| Done
File --> Done(["结束"])
sequenceDiagram
participant App as 业务代码
participant M as JLLogManager
participant C as 控制台
participant F as 日志文件
participant CB as 上层回调
App->>M: kJLLog(JLLOG_INFO, "BT connect %@", addr)
M->>M: 过滤:enable/等级/前缀拼接
M->>C: 控制台输出
alt saveLogAsFile == YES
M->>F: saveLog: 追加写入文件
end
opt 已设置 collectLog:
M-->>CB: block(log 字符串) 实时转发
end
两条路径的设计意图:
- 先过滤后格式化:等级与使能判断在字符串拼接之前完成,低等级日志被直接丢弃,避免无谓的字符串格式化开销——这在蓝牙音频等高频率日志场景中很重要。
- 输出解耦:控制台、文件、回调三路输出相互独立。
collectLog:的回调不参与文件写入,上层既可只监听、也可同时开启文件保存,互不阻塞。
配置选项
JLLogHelper 的配置全部通过 JLLogManager 类方法在运行时设置,无独立配置文件。常用配置项如下:
| 配置项 | 方法 | 类型/取值 | 默认值 | 说明 |
|---|---|---|---|---|
| 日志使能与等级 | setLog:IsMore:Level: | enable: BOOL;isMore: BOOL;level: JLLOG_LEVEL | 开启;JLLOG_DEBUG | 总开关 + 等级阈值 + 是否打印函数名/行号 |
| 时间戳打印 | logWithTimestamp: | BOOL | NO | 是否在日志前缀中打印时间 |
| 文件保存 | saveLogAsFile: | BOOL | YES | 是否把日志同时保存为文件 |
| 保存路径 | redirectLogPath: | NSString 路径 | 框架默认路径 | 重定向日志文件保存目录 |
| 日志收集回调 | collectLog: | void(^)(NSString *log) | nil(未设置) | 每条日志实时回调给上层 |
| 打印 SDK 版本 | sdkVersion | — | — | 输出框架版本信息 |
| 清理日志 | clearLog | — | — | 清空已存储的日志 |
| 打开日志文本 | openLogTextFile | — | — | 打印文本日志(便于查看文件内容) |
API Reference
全部接口均为类方法,定义于 JLLogManager.h。
+ (void)sdkVersion
打印当前 JLLogHelper SDK 版本到控制台。用于集成自检,确认 Demo 与发布包版本一致。
+ (void)clearLog
清理已存储的日志文件内容。在开启 saveLogAsFile: 的场景下,可用于分段抓取日志(例如每轮设备连接前清空一次)。
+ (void)openLogTextFile
打开/打印文本日志。配合文件保存使用,便于直接查看当前日志文件内容。
+ (void)redirectLogPath:(NSString *)path
参数: path(NSString)——日志文件保存目录,需为可写路径。
重定向日志文件保存位置。默认路径由框架内部管理;当 App 需要把日志统一收进自己的沙盒目录(如 Documents/Logs)以便上传或备份时必须调用此方法。
+ (void)collectLog:(void(^)(NSString *log))block
参数: block——日志回调,接收格式化后的完整日志字符串。
注册日志收集回调。设置后每条通过框架输出的日志都会以字符串形式回调给上层;传入 nil 可取消。典型用途:SDK 联调时把日志转发到自定义显示控件,或在崩溃前把日志批量上报。
+ (void)logWithTimestamp:(BOOL)isTimeStamp
参数: isTimeStamp(BOOL)——是否打印时间戳,默认 NO。
开启后日志输出会携带时间前缀,便于分析耗时问题(如蓝牙连接时序、音频数据缓冲节奏)。
+ (void)saveLogAsFile:(BOOL)isSave
参数: isSave(BOOL)——是否保存为文件,默认 YES。
控制日志是否同时写入文件。发布版可置 NO 以减少 IO;联调期保持 YES 以便追溯历史日志。
+ (void)setLog:(BOOL)enable IsMore:(BOOL)isMore Level:(JLLOG_LEVEL)level
参数:
enable(BOOL)——日志总开关,NO 时所有日志丢弃;isMore(BOOL)——是否打印【函数名 & 行号】;level(JLLOG_LEVEL)——日志等级阈值,低于该等级的日志不输出。
设计意图: 这是框架最核心的运行时开关。isMore 依赖 kJLLog 宏注入的 __FUNCTION__/__LINE__,若直接调用 logLevel:content: 则无法附带函数名行号。
+ (void)logLevel:(JLLOG_LEVEL)level funcName:(const char * _Nullable)func line:(const int)line format:(NSString * _Nonnull)format, ...
参数:
level(JLLOG_LEVEL)——本条日志等级;func(const char *,可空)——函数名;line(int)——行号;format(NSString *)——格式化字符串,后接可变参数。
kJLLog 宏的目标方法,是框架内部统一的打印入口。业务代码通常不应直接调用,而应使用宏以保证函数名/行号正确注入。
+ (void)logSomething:(NSString *)something
参数: something(NSString *)——任意字符串内容。
无等级、无格式化的简易打印,适合临时打点。
+ (void)logObject:(id)object
参数: object(id)——任意对象。
直接打印对象(内部走 description)。适合快速输出字典、数组、模型对象。
+ (void)logLevel:(JLLOG_LEVEL)level content:(NSString *)content
参数:
level(JLLOG_LEVEL)——日志等级;content(NSString *)——已格式化的完整内容。
Swift 桥接友好的带等级打印入口,不携带函数名/行号信息。
+ (void)saveLog:(NSString *)logContent
参数: logContent(NSString *)——日志内容。
手动把一段日志内容写入日志文件,不经过控制台输出。可用于把自定义业务事件(如外设状态机切换)追加进日志文件,保持日志时间线的完整性。
使用示例
集成方式
在工程的预编译头文件(.pch)中全局引入,即可在任意文件中直接使用宏(Demo 工程即采用此方式,见 JieLiAppHeader.pch):
#import <JLLogHelper/JLLogHelper.h>
基础用法:宏打印
// 全量日志:不过滤,用于协议抓包
kJLLog(JLLOG_COMPLETE, @"raw data: %@", hexData);
// 调试日志(框架默认等级)
kJLLog(JLLOG_DEBUG, @"device connected, name = %@", deviceName);
// 错误日志
kJLLog(JLLOG_ERROR, @"send command failed, code = %d", code);
宏自动携带 __FUNCTION__ 与 __LINE__,配合 setLog:IsMore: 可输出形如 [ClassName methodName:line] 的定位前缀。
初始化与开关配置
// 开启日志,打印函数名/行号,等级阈值设为 INFO(DEBUG 以下被过滤)
[JLLogManager setLog:YES IsMore:YES Level:JLLOG_INFO];
// 开启时间戳,便于分析时序
[JLLogManager logWithTimestamp:YES];
// 把日志文件重定向到 App 沙盒目录
NSString *logDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
[JLLogManager redirectLogPath:[logDir stringByAppendingPathComponent:@"Logs"]];
// 注册收集回调:把日志实时转发给 UI 或上报模块
[JLLogManager collectLog:^(NSString *log) {
// 例如:追加到调试悬浮窗 / 写入上传队列
}];
// 确认 SDK 版本
[JLLogManager sdkVersion];
简易打印与对象打印
// 任意字符串打点
[JLLogManager logSomething:@"enter scan flow"];
// 直接打印对象(内部走 description)
[JLLogManager logObject:connectedDevice];
[JLLogManager logObject:@{@"state": @(state), @"rssi": @(rssi)}];
// Swift 友好的带等级入口
[JLLogManager logLevel:JLLOG_WARN content:@"battery low"];
失败模式、边界情况与并发
- 等级语义陷阱:
JLLOG_COMPLETE = 0是"最低严重度、最大日志量"的档位。若在setLog:IsMore:Level:中误把阈值设为JLLOG_COMPLETE,则所有等级(含 ERROR)都会输出,日志量可能急剧膨胀,影响性能与文件体积。 isMore依赖宏:只有通过kJLLog宏打印才能获得函数名/行号;直接调用logLevel:content:或logSomething:时,即使isMore为 YES 也没有位置信息可注入,排查时需注意区分。- 回调重入:
collectLog:的 block 在日志打印路径中同步执行。若在 block 内部再次调用日志打印(例如把日志格式化为新字符串后再logSomething:),会形成无限递归导致栈溢出。上层应只做转发/存储,不做二次打日志。 - 文件 IO 失败:
redirectLogPath:若传入不可写目录(如只读 bundle 路径),文件保存会静默失败或抛异常;应在写入前确认目录存在且可写,必要时clearLog重试。 - 并发访问:
JLLogManager全局状态(开关、等级、路径、回调)由类方法读写,若在多个线程同时配置与打印,存在竞争条件。日志打印本身通常不保证线程安全顺序,多线程场景下日志行序可能交错;建议在配置阶段(启动早期)集中设置,运行期只做打印。 - 发布版本号校验:
JLLogHelper.h导出的JLLogHelperVersionNumber/JLLogHelperVersionString用于版本自检;集成时若日志行为与文档不符,可先通过sdkVersion确认为最新版。
性能与运维考量
- 过滤前置:等级过滤先于字符串格式化,是框架性能设计的核心——低频关注点(如每次回调的
logObject:)可放心使用,高频点(如音频帧、BLE 数据包)建议用JLLOG_COMPLETE/JLLOG_DEBUG并在发布前调高阈值或setLog:NO关闭。 - 文件 IO:
saveLogAsFile:默认 YES,会在每次日志时追加写文件;长期运行时建议按需开启(联调期),发布版置 NO,或配合redirectLogPath:将日志落到便于清理的目录,并用clearLog分段管理。 - 多平台切片:框架通过
JLLogHelper.xcframework同时提供ios-arm64、ios-arm64_x86_64-simulator、macos-arm64_x86_64切片(见 Info.plist),真机、模拟器、Mac Catalyst 场景均可直接集成;打包发布时使用Release/Libs/JLLogHelper.xcframework并设置 Embed & Sign。 - 日志脱敏:
collectLog:会把日志原样透传给上层,若日志中包含 MAC 地址、设备名称等敏感信息,上层回调中需自行脱敏后再上传。
扩展点
- 输出通道扩展:框架通过
collectLog:回调提供了唯一的自定义输出扩展点,上层可以实现日志悬浮窗、滚动视图、网络上报、崩溃文件附加等任意下游,而无需修改框架本身。 - 路径策略定制:
redirectLogPath:允许把日志归入 App 自己的沙盒体系,配合clearLog/openLogTextFile可实现按会话、按设备的分段日志管理。 - Swift 互操作:
logLevel:content:、logSomething:、logObject:等无可变参数的方法对 Swift 调用友好,Swift 工程可用JLLogManager.logLevel(JLLOG_LEVEL.info, content: "...")的方式接入。
Related Links
- JLLogManager.h(API 定义)
- JLLogHelper.h(框架入口头文件)
- JieLiAppHeader.pch(Demo 集成方式)
- JLLogHelper.xcframework Info.plist(平台切片声明)
- README.md(仓库目录结构与框架清单)
- 相关框架:
JLAudioUnitKit(音频处理)、JLBmpConvertKit(图像转换)、JLPackageResKit(资源包处理)——详见各自的 SDK 框架目录页。