日志系统 (Log System) 是 AliPlayerKit 的基础支撑模块。它通过统一的日志中心 LogHub,提供日志输出、级别过滤、观察者机制等核心能力,实现播放器运行时信息的可视化与可追溯。
概念介绍
什么是 LogHub?
LogHub 是 AliPlayerKit 中所有日志输出的统一入口,采用单例模式设计。它封装了 iOS 原生日志 API(NSLog),在保留原有功能的基础上,增加了以下核心能力:
能力 | 说明 |
日志开关 | 控制日志输出是否启用(总开关) |
控制台开关 | 控制日志是否输出到 Xcode Console |
级别过滤 | 只输出指定级别及以上的日志 |
观察者机制 | 支持外部观察者接收日志输出,实现自定义处理(如写入文件) |
什么是日志观察者?
日志观察者 (LogObserver) 是用于接收 LogHub 输出日志的回调协议。通过实现该协议,可以自定义日志的处理方式,例如:
将日志写入文件,便于问题排查时提供给技术支持团队
将日志上传到远程服务器,实现远程日志收集
在 UI 上展示日志,用于调试面板
混合线程模型
LogHub 采用混合线程模型:
操作 | 线程 |
线程名和时间戳捕获 | 调用线程同步执行 |
控制台输出(NSLog) | 调用线程同步执行 |
观察者通知 | 后台串行队列异步分派 |
LogObserver.onLog: 回调在后台串行队列执行,如需更新 UI 请切换到主线程。
功能特性
核心能力
能力 | 说明 |
多级别日志 | 支持 Verbose、Debug、Info、Warn、Error 五个级别 |
三重控制 | 全局开关 + 级别过滤 + 控制台开关,精确控制输出范围 |
观察者机制 | 支持注册自定义观察者,实现日志持久化、远程上报等 |
便捷宏 | 提供 |
日志面板 | 通过 |
全局快捷 API | 通过 |
核心组件
组件 | 类型 | 说明 |
| 单例 | 日志中心,统一日志输出入口 |
| 枚举 | 日志级别定义 |
| 数据类 | 日志信息模型,封装单条日志的完整信息 |
| 协议 | 日志观察者接口 |
| 插槽 | 播放器内嵌日志面板,实时展示关键日志 |
使用方式
基本用法 — 全局快捷配置
通过 AliPlayerKit 类方法快速配置日志系统(推荐方式):
// 在 AppDelegate 中,setup 之后配置
[AliPlayerKit setup];
// 设置日志级别
[AliPlayerKit setLogLevel:LogLevelVerbose];
// 启用/禁用控制台日志
[AliPlayerKit enableConsoleLog:YES];AliPlayerKit 方法 | 说明 |
| 设置日志级别,低于该级别的日志被忽略(默认 |
| 获取当前日志级别 |
| 启用/禁用控制台输出(默认启用) |
| 查询控制台日志是否启用 |
| 开启/关闭调试模式 |
| 查询调试模式是否开启 |
| 启用/禁用日志面板插槽(默认禁用) |
| 查询日志面板是否启用 |
基本用法 — 输出日志
使用 SLOG 系列便捷宏输出各级别日志,宏会自动捕获源文件名和行号:
#import <AliPlayerKit/LogMacros.h>
// 输出各级别日志(自动捕获 __FILE_NAME__ 和 __LINE__)
SLOGV(@"viewDidLoad called");
SLOGD(@"配置播放器: videoId=%@", videoId);
SLOGI(@"开始播放视频: %@", videoTitle);
SLOGW(@"缓冲中,当前进度: %.1f%%", progress * 100);
SLOGE(@"播放失败,错误码: %ld", (long)errorCode);便捷宏列表:
宏 | 级别 | 说明 |
| Verbose | 最详细级别日志 |
| Debug | 调试信息 |
| Info | 一般信息 |
| Warn | 潜在问题 |
| Error | 错误信息 |
SLOG 宏内部调用 LogHub 的 logLevel:file:line:message:error: 方法,自动通过 __FILE_NAME__ 和 __LINE__ 捕获源码位置,无需手动传入 tag。
也可以直接调用 LogHub 实例方法,手动指定 tag:
// 使用 tag 标识日志来源
[[LogHub sharedHub] i:@"PlayerVC" msg:@"开始播放"];
[[LogHub sharedHub] e:@"MediaPlayer" msg:@"播放失败" error:error];基本用法 — 配置日志级别
通过 LogHub 直接配置日志属性:
LogHub *logHub = [LogHub sharedHub];
#ifdef DEBUG
// 开发环境:详细日志 + 控制台输出
logHub.logEnabled = YES;
logHub.consoleLogEnabled = YES;
logHub.logLevel = LogLevelVerbose;
#else
// 生产环境:精简日志 + 关闭控制台
logHub.logEnabled = YES;
logHub.consoleLogEnabled = NO;
logHub.logLevel = LogLevelInfo;
#endif三重控制机制:
控制层 | 属性 | 说明 |
总开关 |
| 关闭后所有日志静默(包括观察者通知) |
级别过滤 |
| 低于该级别的日志被忽略(默认 |
控制台开关 |
| 控制是否通过 NSLog 输出到 Xcode Console |
关闭 consoleLogEnabled 后,日志仍会通过观察者通知分发,仅抑制控制台输出。关闭 logEnabled 后,所有日志请求被完全忽略,观察者也不会收到通知。
高级用法 — 注册日志观察者
通过实现 LogObserver 协议自定义日志处理:
// 创建并注册文件日志观察者
FileLogObserver *fileObserver = [[FileLogObserver alloc] init];
[[LogHub sharedHub] addObserver:fileObserver];
// 移除观察者(不再需要时)
[[LogHub sharedHub] removeObserver:fileObserver];LogHub 以强引用方式持有观察者,直到通过 removeObserver: 显式移除。因此必须在适当时机(如 dealloc 或 viewWillDisappear:)调用 removeObserver: 以避免循环引用。
高级用法 — 日志面板
LogPanelSlot 是一个内置的调试插槽,可在播放器视图中实时展示关键日志信息:
// 启用日志面板(建议仅在调试场景中启用)
[AliPlayerKit setLogPanelEnabled:YES];交互 | 说明 |
点击标题栏 | 展开/折叠日志内容区 |
点击"清除"按钮 | 清空已展示的日志 |
长按日志区域 | 将日志内容拷贝到剪贴板 |
日志面板仅显示 Debug 及以上级别的日志。需要在 rebuildSlots 之前设置 setLogPanelEnabled:。在 Minimal 场景中不可见。
自定义开发
日志观察者协议 (LogObserver)
@protocol LogObserver <NSObject>
@required
/// 日志输出回调
/// @param logInfo 日志信息对象,包含级别、标签、消息和时间戳
/// @warning 回调在后台串行队列执行,如需更新 UI 请 dispatch 到主线程
- (void)onLog:(LogInfo *)logInfo;
@endLogInfo数据模型
属性 | 类型 | 说明 |
|
| 日志级别(readonly) |
|
| 日志标签(readonly) |
|
| 日志消息内容(readonly) |
|
| 日志生成时间戳(readonly) |
|
| 生成日志的线程名称(readonly) |
|
| 源文件名,如 |
|
| 源代码行号(readonly) |
|
| 关联的错误对象,可为 nil(readonly) |
方法 | 说明 |
| 获取格式化的日志字符串 |
| 指定初始化方法 |
| 便捷初始化方法(自动捕获时间戳和线程名) |
格式化输出格式:
2024-04-22 10:30:00.123 I/AliPlayerKit [version] [FileName:line] [threadName] : message如果存在错误,将附加额外一行:
Error: domain=xxx code=xxx userInfo=xxx自定义文件日志观察者示例
以下是一个文件日志观察者的完整实现:
@interface MyFileLogObserver : NSObject <LogObserver>
- (instancetype)initWithLogDirectory:(NSString *)directory;
@end
@implementation MyFileLogObserver {
NSString *_logFilePath;
dispatch_queue_t _writeQueue;
NSFileHandle *_fileHandle;
}
- (instancetype)initWithLogDirectory:(NSString *)directory {
self = [super init];
if (self) {
_writeQueue = dispatch_queue_create("com.example.filelog",
DISPATCH_QUEUE_SERIAL);
[self setupLogFileAtDirectory:directory];
}
return self;
}
- (void)setupLogFileAtDirectory:(NSString *)directory {
NSFileManager *fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:directory]) {
[fm createDirectoryAtPath:directory
withIntermediateDirectories:YES
attributes:nil
error:nil];
}
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"yyyy-MM-dd";
NSString *fileName = [NSString stringWithFormat:@"player_%@.log",
[formatter stringFromDate:[NSDate date]]];
_logFilePath = [directory stringByAppendingPathComponent:fileName];
if (![fm fileExistsAtPath:_logFilePath]) {
[fm createFileAtPath:_logFilePath contents:nil attributes:nil];
}
_fileHandle = [NSFileHandle fileHandleForWritingAtPath:_logFilePath];
[_fileHandle seekToEndOfFile];
}
#pragma mark - LogObserver
- (void)onLog:(LogInfo *)logInfo {
NSString *formatted = [logInfo formattedString];
dispatch_async(_writeQueue, ^{
NSString *line = [formatted stringByAppendingString:@"\n"];
NSData *data = [line dataUsingEncoding:NSUTF8StringEncoding];
@try {
[self->_fileHandle writeData:data];
} @catch (NSException *exception) {
// 忽略写入异常
}
});
}
- (void)dealloc {
[_fileHandle closeFile];
}
@endonLog: 回调已在后台串行队列执行,但文件 I/O 仍建议使用独立的写入队列,避免阻塞日志分发队列影响其他观察者。
完整集成示例
// AppDelegate.m
#import <AliPlayerKit/AliPlayerKit.h>
@interface AppDelegate ()
@property (nonatomic, strong) MyFileLogObserver *fileLogObserver;
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 1. 初始化 AliPlayerKit
[AliPlayerKit setup];
// 2. 配置日志系统
#ifdef DEBUG
[AliPlayerKit setLogLevel:LogLevelVerbose];
[AliPlayerKit enableConsoleLog:YES];
// 可选:启用日志面板
[AliPlayerKit setLogPanelEnabled:YES];
#else
[AliPlayerKit setLogLevel:LogLevelInfo];
[AliPlayerKit enableConsoleLog:NO];
// 3. 注册文件日志观察者(仅生产环境)
NSString *logDir = [NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES).firstObject
stringByAppendingPathComponent:@"PlayerLogs"];
self.fileLogObserver = [[MyFileLogObserver alloc] initWithLogDirectory:logDir];
[[LogHub sharedHub] addObserver:self.fileLogObserver];
#endif
return YES;
}
@endPlayerKitUsages/UsageLogSystem 模块提供了完整的日志系统使用示例,包含 FileLogObserver 文件日志观察者和 UI 日志展示。
最佳实践
环境配置建议
环境 | 总开关 | 控制台 | 级别 | 观察者 | 日志面板 |
开发调试 | YES | YES | Verbose | 可选(UI 面板) | 可选 |
内部测试 | YES | YES | Debug | 文件日志 | NO |
生产环境 | YES | NO | Info | 文件日志 + 远程上报 | NO |
敏感场景 | NO | — | — | — | NO |
常见错误
错误 1:观察者回调中直接更新 UI
// ✗ 错误:onLog: 在后台串行队列执行,直接更新 UI 会崩溃
- (void)onLog:(LogInfo *)logInfo {
self.logLabel.text = [logInfo formattedString]; // 崩溃!
}
// ✓ 正确:切换到主线程更新 UI
- (void)onLog:(LogInfo *)logInfo {
NSString *formatted = [logInfo formattedString];
dispatch_async(dispatch_get_main_queue(), ^{
self.logLabel.text = formatted;
});
}错误 2:观察者中同步阻塞操作
// ✗ 错误:在回调中同步写文件,阻塞日志分发队列
- (void)onLog:(LogInfo *)logInfo {
NSString *log = [logInfo formattedString];
[log writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
// ✓ 正确:异步写入文件
- (void)onLog:(LogInfo *)logInfo {
NSString *log = [logInfo formattedString];
dispatch_async(_writeQueue, ^{
// 异步写入...
});
}错误 3:忘记移除观察者导致循环引用
// LogHub 弱引用观察者,不移除不会造成循环引用,但可能在页面消失后仍收到日志回调
- (void)viewDidLoad {
[[LogHub sharedHub] addObserver:self];
}
// ✓ 推荐:在生命周期结束时主动移除
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[LogHub sharedHub] removeObserver:self];
}与通知系统的 Dispatcher 类似,LogHub 以弱引用方式持有观察者,因此不会造成内存泄漏;调用 removeObserver: 主要是为了避免在观察者生命周期结束后继续收到无意义的日志回调。
API 速查
日志级别 (LogLevel)
级别 | 枚举值 | 数值 | 说明 |
Verbose |
| 0 | 最详细,开发调试 |
Debug |
| 1 | 调试信息 |
Info |
| 2 | 默认级别,生产推荐 |
Warn |
| 3 | 潜在问题 |
Error |
| 4 | 错误信息 |
None |
| 100 | 禁用所有日志 |
LogLevel.h 同时提供了 LogLevelToString() 内联函数,将级别转为缩写字符串(V/D/I/W/E/N)。
LogHub 核心 API
属性 / 方法 | 类型 | 说明 |
| 类方法 | 获取单例 |
|
| 日志总开关(默认 YES) |
|
| 日志级别(默认 |
|
| 控制台开关(默认 YES) |
| 实例方法 | 输出 Verbose 日志 |
| 实例方法 | 输出 Debug 日志 |
| 实例方法 | 输出 Info 日志 |
| 实例方法 | 输出 Warn 日志 |
| 实例方法 | 输出 Error 日志 |
| 实例方法 | 输出 Error 日志,附带 |
| 实例方法 | 按指定级别输出 |
| 实例方法 | 核心日志方法(供 SLOG 宏使用) |
| 实例方法 | 添加观察者(弱引用,重复添加无效果) |
| 实例方法 | 移除观察者 |
| 类方法 | 级别转字符串(V/D/I/W/E/N) |
| 类方法 | 字符串转级别(不区分大小写,无法识别时返回 |
AliplayerKit日志快捷API
方法 | 说明 |
| 设置日志级别(需在 |
| 获取当前日志级别 |
| 启用/禁用控制台日志输出 |
| 查询控制台日志是否启用 |
| 开启/关闭调试模式 |
| 查询调试模式是否开启 |
| 启用/禁用日志面板插槽 |
| 查询日志面板是否启用 |
SLOG便捷宏
宏 | 级别 | 展开形式 |
| Verbose |
|
| Debug | 同上,级别为 Debug |
| Info | 同上,级别为 Info |
| Warn | 同上,级别为 Warn |
| Error | 同上,级别为 Error |
LogInfo属性
属性 | 类型 | 说明 |
|
| 日志级别 |
|
| 日志标签 |
|
| 日志消息内容 |
|
| 日志生成时间戳 |
|
| 线程名称 |
|
| 源文件名 |
|
| 源代码行号 |
|
| 关联的错误对象(nullable) |
| 方法 | 获取格式化的日志字符串 |
LogObserver 协议
方法 | 必选 | 说明 |
| 是( | 日志输出回调,在后台串行队列执行 |