導(dǎo)語(yǔ):
DDLog光酣,即CocoaLumberjack是iOS開(kāi)發(fā)用的最多的日志框架苔货,出自大神Robbie Hanson之手(還有諸多知名開(kāi)源框架如 XMPPFramework狂男、 CocoaAsyncSocket埠忘,都是即時(shí)通信領(lǐng)域很基礎(chǔ)應(yīng)用很多的框架)脾拆。了解DDLog的源碼將有助于我們更好的輸出代碼中的日志信息,便于定位問(wèn)題莹妒,也能對(duì)我們?cè)跁?shū)寫(xiě)自己的日志框架或者其他模塊時(shí)有所啟發(fā)名船。
此系列文章將分為以下幾篇:
- DDLog源碼解析一:框架結(jié)構(gòu)
- DDLog源碼解析二:設(shè)計(jì)初衷
- DDLog源碼解析三:FileLogger
本文將對(duì)DDLog支持的眾多Logger中值得分析的文件logger(其余l(xiāng)ogger基本只涉及系統(tǒng)api的調(diào)用)進(jìn)行分析,并簡(jiǎn)要分析一些雜亂的知識(shí)點(diǎn)旨怠。
FileLogger初始化
FileLogger初始化包含兩種初始化操作:默認(rèn)配置和自定義配置
- (instancetype)init {
DDLogFileManagerDefault *defaultLogFileManager = [[DDLogFileManagerDefault alloc] init];
return [self initWithLogFileManager:defaultLogFileManager];
}
- (instancetype)initWithLogFileManager:(id <DDLogFileManager>)aLogFileManager {
if ((self = [super init])) {
_maximumFileSize = kDDDefaultLogMaxFileSize;
_rollingFrequency = kDDDefaultLogRollingFrequency;
_automaticallyAppendNewlineForCustomFormatters = YES;
logFileManager = aLogFileManager;
self.logFormatter = [DDLogFileFormatterDefault new];
}
return self;
}
FileLogger默認(rèn)配置
FileLogger默認(rèn)配置由DDLogFileManagerDefault來(lái)實(shí)現(xiàn)渠驼,DDLogFileManagerDefault類(lèi)中除可以定義日志文件保存路徑外,其余信息都屬于寫(xiě)死的固定值(包括下面的靜態(tài)常量):
// 日志文件數(shù)的最大值
NSUInteger const kDDDefaultLogMaxNumLogFiles = 5; // 5 Files
// 日志文件占用空間最大值
unsigned long long const kDDDefaultLogFilesDiskQuota = 20 * 1024 * 1024; // 20 MB
// 日志默認(rèn)路徑為沙盒中caches文件中的Logs文件夾
- (NSString *)defaultLogsDirectory {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *baseDir = paths.firstObject;
NSString *logsDirectory = [baseDir stringByAppendingPathComponent:@"Logs"];
return logsDirectory;
}
同時(shí)鉴腻,DDLogFileManagerDefault的實(shí)例初始化時(shí)還對(duì)兩個(gè)變量通過(guò)KVO形式進(jìn)行監(jiān)聽(tīng)變化:
[self addObserver:self forKeyPath:NSStringFromSelector(@selector(maximumNumberOfLogFiles)) options:kvoOptions context:nil];
[self addObserver:self forKeyPath:NSStringFromSelector(@selector(logFilesDiskQuota)) options:kvoOptions context:nil];
如果將一個(gè)對(duì)象設(shè)定成屬性,這個(gè)屬性是自動(dòng)支持KVO的,如果這個(gè)對(duì)象是一個(gè)實(shí)例變量,那么,這個(gè)KVO是需要我們自己來(lái)實(shí)現(xiàn)的. 所以這里對(duì)maximumNumberOfLogFiles和logFilesDiskQuota重寫(xiě)了
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey
來(lái)支持KVO:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey
{
BOOL automatic = NO;
if ([theKey isEqualToString:@"maximumNumberOfLogFiles"] || [theKey isEqualToString:@"logFilesDiskQuota"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
當(dāng)KVO監(jiān)聽(tīng)到兩個(gè)實(shí)例變量的變化時(shí)迷扇,需要通過(guò)- (void)deleteOldLogFiles方法來(lái)判斷是否需要?jiǎng)h除文件以滿(mǎn)足最新的文件數(shù)量和大小的要求百揭,但刪除的時(shí)候需要注意,如果只剩一個(gè)文件待刪除蜓席,判斷到該文件未歸檔信峻,則不能刪除,因?yàn)榇宋募赡苷趯?xiě)入信息瓮床,還沒(méi)有關(guān)閉文件盹舞。 代碼片段如下:
if (firstIndexToDelete == 0) {
// Do we consider the first file?
// We are only supposed to be deleting archived files.
// In most cases, the first file is likely the log file that is currently being written to.
// So in most cases, we do not want to consider this file for deletion.
if (sortedLogFileInfos.count > 0) {
DDLogFileInfo *logFileInfo = sortedLogFileInfos[0];
if (!logFileInfo.isArchived) {
// Don't delete active file.
++firstIndexToDelete;
}
}
}
文件命名
默認(rèn)的文件名命名方式:app名稱(chēng)為前綴,加上經(jīng)過(guò)一定格式format過(guò)的格式隘庄。
- (NSDateFormatter *)logFileDateFormatter {
NSMutableDictionary *dictionary = [[NSThread currentThread]
threadDictionary];
NSString *dateFormat = @"yyyy'-'MM'-'dd'--'HH'-'mm'-'ss'-'SSS'";
NSString *key = [NSString stringWithFormat:@"logFileDateFormatter.%@", dateFormat];
NSDateFormatter *dateFormatter = dictionary[key];
if (dateFormatter == nil) {
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
[dateFormatter setDateFormat:dateFormat];
[dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
dictionary[key] = dateFormatter;
}
return dateFormatter;
}
- (NSString *)newLogFileName {
NSString *appName = [self applicationName];
NSDateFormatter *dateFormatter = [self logFileDateFormatter];
NSString *formattedDate = [dateFormatter stringFromDate:[NSDate date]];
return [NSString stringWithFormat:@"%@ %@.log", appName, formattedDate];
}
- (NSString *)applicationName {
static NSString *_appName;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"];
if (!_appName) {
_appName = [[NSProcessInfo processInfo] processName];
}
if (!_appName) {
_appName = @"";
}
});
return _appName;
}
這里需要注意applicationName的獲取方式中使用了dispatch_once踢步,是為了保證線程安全和低負(fù)載:NSProcessInfo是線程不安全的,而這個(gè)方法可能在多個(gè)線程中同時(shí)訪問(wèn)到NSProcessInfo丑掺。而低負(fù)載是指這部分信息其實(shí)是app的通用信息获印,不會(huì)改變,所以復(fù)制到靜態(tài)變量中街州,不管哪個(gè)實(shí)例變量來(lái)獲取兼丰,都可以通過(guò)第一次獲取的值直接給它。
FileLogger重要邏輯
FileLogger還要在初始化時(shí)配置單個(gè)文件大小的最大值和輪詢(xún)檢查文件時(shí)間(這兩個(gè)值已寫(xiě)死)
unsigned long long const kDDDefaultLogMaxFileSize = 1024 * 1024; // 1 MB
NSTimeInterval const kDDDefaultLogRollingFrequency = 60 * 60 * 24; // 24 Hours
_maximumFileSize = kDDDefaultLogMaxFileSize;
_rollingFrequency = kDDDefaultLogRollingFrequency;
由于這兩個(gè)值直接跟寫(xiě)日志相關(guān)唆缴,所以這兩個(gè)值的getter和setter方法都使用了上一節(jié)解析的線程保護(hù)方式:先在全局日志隊(duì)列排隊(duì)鳍征,再到自己的日志隊(duì)列中排隊(duì)進(jìn)行操作,以一個(gè)為例:
- (NSTimeInterval)rollingFrequency {
__block NSTimeInterval result;
dispatch_block_t block = ^{
result = _rollingFrequency;
};
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
dispatch_sync(globalLoggingQueue, ^{
dispatch_sync(self.loggerQueue, block);
});
return result;
}
當(dāng)輪詢(xún)檢查需要檢查文件兩方面信息:大小和已經(jīng)打開(kāi)的時(shí)間面徽。文件大小通過(guò)NSFileHandle的方法可以判斷艳丛,而已經(jīng)打開(kāi)的時(shí)間則需要通過(guò)計(jì)時(shí)器來(lái)計(jì)時(shí),最終在輪詢(xún)時(shí)刻與設(shè)定的值比較趟紊。到當(dāng)前文件已經(jīng)符合設(shè)置的值時(shí)氮双,需要關(guān)閉文件,并將文件歸檔霎匈,再將文件計(jì)時(shí)器關(guān)閉戴差。
文件權(quán)限
在寫(xiě)日志文件前需要?jiǎng)?chuàng)建新文件,由于iOS系統(tǒng)默認(rèn)設(shè)置文件權(quán)限為NSFileProtectionCompleteUnlessOpen铛嘱, 但如果app可以在后臺(tái)運(yùn)行暖释,需要設(shè)置為NSFileProtectionCompleteUntilFirstUserAuthentication,才能保證即使鎖屏也能正常創(chuàng)建和讀寫(xiě)文件弄痹。
//文件未受保護(hù)饭入,隨時(shí)可以訪問(wèn) (Default)
NSFileProtectionNone
//文件受到保護(hù),而且只有在設(shè)備未被鎖定時(shí)才可訪問(wèn)
NSFileProtectionComplete
//文件收到保護(hù)肛真,直到設(shè)備啟動(dòng)且用戶(hù)第一次輸入密碼
NSFileProtectionCompleteUntilFirstUserAuthentication
//文件受到保護(hù),而且只有在設(shè)備未被鎖定時(shí)才可打開(kāi)爽航,不過(guò)即便在設(shè)備被鎖定時(shí)蚓让,已經(jīng)打開(kāi)的文件還是可以繼續(xù)使用和寫(xiě)入
NSFileProtectionCompleteUnlessOpen
而其中app是否可以在app后臺(tái)運(yùn)行乾忱,是通過(guò)plist中對(duì)應(yīng)的配置項(xiàng)是否申請(qǐng)了后臺(tái)運(yùn)行能力來(lái)判斷的:
BOOL doesAppRunInBackground() {
BOOL answer = NO;
NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
for (NSString *mode in backgroundModes) {
if (mode.length > 0) {
answer = YES;
break;
}
}
return answer;
}