日誌系統 (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 協議自訂Tlog:
// 建立並註冊檔案日誌觀察者
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 協議
方法 | 必選 | 說明 |
| 是( | 日誌輸出回調,在後台串列隊列執行 |