DDLog源碼解析三:FileLogger

導(dǎo)語:

DDLog,即CocoaLumberjack是iOS開發(fā)用的最多的日志框架,出自大神Robbie Hanson之手(還有諸多知名開源框架如 XMPPFrameworkCocoaAsyncSocket,都是即時(shí)通信領(lǐng)域很基礎(chǔ)應(yīng)用很多的框架)。了解DDLog的源碼將有助于我們更好的輸出代碼中的日志信息,便于定位問題,也能對我們在書寫自己的日志框架或者其他模塊時(shí)有所啟發(fā)。

此系列文章將分為以下幾篇:
- DDLog源碼解析一:框架結(jié)構(gòu)
- DDLog源碼解析二:設(shè)計(jì)初衷
- DDLog源碼解析三:FileLogger

本文將對DDLog支持的眾多Logger中值得分析的文件logger(其余l(xiāng)ogger基本只涉及系統(tǒng)api的調(diào)用)進(jì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來實(shí)現(xiàn),DDLogFileManagerDefault類中除可以定義日志文件保存路徑外,其余信息都屬于寫死的固定值(包括下面的靜態(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í)還對兩個(gè)變量通過KVO形式進(jìn)行監(jiān)聽變化:

[self addObserver:self forKeyPath:NSStringFromSelector(@selector(maximumNumberOfLogFiles)) options:kvoOptions context:nil];
[self addObserver:self forKeyPath:NSStringFromSelector(@selector(logFilesDiskQuota)) options:kvoOptions context:nil];

如果將一個(gè)對象設(shè)定成屬性,這個(gè)屬性是自動(dòng)支持KVO的,如果這個(gè)對象是一個(gè)實(shí)例變量,那么,這個(gè)KVO是需要我們自己來實(shí)現(xiàn)的. 所以這里對maximumNumberOfLogFiles和logFilesDiskQuota重寫了

  • (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey
    來支持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)聽到兩個(gè)實(shí)例變量的變化時(shí),需要通過- (void)deleteOldLogFiles方法來判斷是否需要?jiǎng)h除文件以滿足最新的文件數(shù)量和大小的要求,但刪除的時(shí)候需要注意,如果只剩一個(gè)文件待刪除,判斷到該文件未歸檔,則不能刪除,因?yàn)榇宋募赡苷趯懭胄畔?,還沒有關(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名稱為前綴,加上經(jīng)過一定格式format過的格式。

- (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í)訪問到NSProcessInfo。而低負(fù)載是指這部分信息其實(shí)是app的通用信息,不會(huì)改變,所以復(fù)制到靜態(tài)變量中,不管哪個(gè)實(shí)例變量來獲取,都可以通過第一次獲取的值直接給它。

FileLogger重要邏輯

FileLogger還要在初始化時(shí)配置單個(gè)文件大小的最大值和輪詢檢查文件時(shí)間(這兩個(gè)值已寫死)

unsigned long long const kDDDefaultLogMaxFileSize      = 1024 * 1024;      // 1 MB
NSTimeInterval     const kDDDefaultLogRollingFrequency = 60 * 60 * 24;     // 24 Hours

_maximumFileSize = kDDDefaultLogMaxFileSize;
_rollingFrequency = kDDDefaultLogRollingFrequency;

由于這兩個(gè)值直接跟寫日志相關(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)輪詢檢查需要檢查文件兩方面信息:大小和已經(jīng)打開的時(shí)間。文件大小通過NSFileHandle的方法可以判斷,而已經(jīng)打開的時(shí)間則需要通過計(jì)時(shí)器來計(jì)時(shí),最終在輪詢時(shí)刻與設(shè)定的值比較。到當(dāng)前文件已經(jīng)符合設(shè)置的值時(shí),需要關(guān)閉文件,并將文件歸檔,再將文件計(jì)時(shí)器關(guān)閉。

文件權(quán)限

在寫日志文件前需要?jiǎng)?chuàng)建新文件,由于iOS系統(tǒng)默認(rèn)設(shè)置文件權(quán)限為NSFileProtectionCompleteUnlessOpen, 但如果app可以在后臺(tái)運(yùn)行,需要設(shè)置為NSFileProtectionCompleteUntilFirstUserAuthentication,才能保證即使鎖屏也能正常創(chuàng)建和讀寫文件。

//文件未受保護(hù),隨時(shí)可以訪問 (Default)  
        NSFileProtectionNone

//文件受到保護(hù),而且只有在設(shè)備未被鎖定時(shí)才可訪問                                
        NSFileProtectionComplete 

//文件收到保護(hù),直到設(shè)備啟動(dòng)且用戶第一次輸入密碼                        
        NSFileProtectionCompleteUntilFirstUserAuthentication 

//文件受到保護(hù),而且只有在設(shè)備未被鎖定時(shí)才可打開,不過即便在設(shè)備被鎖定時(shí),已經(jīng)打開的文件還是可以繼續(xù)使用和寫入
    NSFileProtectionCompleteUnlessOpen                      

而其中app是否可以在app后臺(tái)運(yùn)行,是通過plist中對應(yīng)的配置項(xiàng)是否申請了后臺(tái)運(yùn)行能力來判斷的:

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;
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容