DateFormatter深度優化探索

前言

在iOS開發中,對日期進行格式化處理通常有三個步驟:

  • 創建DateFormatter對象
  • 設置日期格式
  • 使用DateFormatter對象對日期進行處理

在上篇文章《DateFormatter性能優化》中,我們通過創建單例對象的方式對創建DateFormatter對象,設置日期格式兩個步驟進行了緩存,將方法耗時降低爲不緩存的方案的10%左右,但是這種優化方法受制於DateFormatter的幾個系統方法的執行效率,本身具有一定的侷限性。之前在一些文章中,也看到了使用C語言的

size_t     strftime_l(char * __restrict, size_t, const char * __restrict,
        const struct tm * __restrict, locale_t)
        __DARWIN_ALIAS(strftime_l) __strftimelike(3);

函數對日期格式化進行處理,所以本文將對以下幾種情況的方法耗時進行評測:

  • 使用Objective-C,不緩存DateFormatter對象
  • 使用Objective-C,緩存DateFormatter對象
  • 使用Objective-C,調用strftime_l做日期處理
  • 使用Swift,不緩存DateFormatter對象
  • 使用Swift,緩存DateFormatter對象
  • 使用Swift,調用strftime_l做日期處理

Objective-C的三種情況下的代碼


//不緩存DateFormatter對象
-(void)testDateFormatterInOCWithoutCache:(NSInteger)times {
    NSString *string = @"";
    NSDate *date;
    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<times; i++) {
        date = [NSDate dateWithTimeIntervalSince1970:(1545308405 + i)];
        
        string = [[DateFormatterCache shareInstance].formatterOne stringFromDate:date];
    }
    CFAbsoluteTime duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
    NSLog(@"不緩存DateFormatter對象的方案計算%ld次,耗時%f ms", (long)times, duration);
}

//緩存DateFormatter對象
-(void)testDateFormatterInOCWithCache:(NSInteger)times {
    NSString *string = @"";
    NSDate *date;
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"yyyy年MM月dd日HH時mm分ss秒"];
    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<times; i++) {
        date = [NSDate dateWithTimeIntervalSince1970:(1545308405 + i)];
        string = [dateFormatter stringFromDate:date];
    }
    CFAbsoluteTime duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
    NSLog(@"緩存DateFormatter對象的方案計算%ld次,耗時%f ms", (long)times, duration);
}

//使用C語言來做日期處理
-(void)testDateFormatterInC:(NSInteger)times {
    NSString *string = @"";
    NSDate *date;
    time_t timeInterval;
    char buffer[80];
    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<times; i++) {
        date = [NSDate dateWithTimeIntervalSince1970:(1545308405 + i)];
        timeInterval = [date timeIntervalSince1970];
        strftime(buffer, sizeof(buffer), "%Y年%m月%d日%H時%M分%S秒", localtime(&timeInterval));
        string = [NSString stringWithCString:buffer encoding:NSUTF8StringEncoding];
    }
    CFAbsoluteTime duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
    NSLog(@"==%@", string);
    NSLog(@"使用C語言的方案計算%ld次,耗時%f ms", (long)times, duration);
}

這裏對於咱們iOS開發的同學來說比較陌生的就是
strftime_l(buffer, sizeof(buffer), "%Y年%m月%d日%H時%M分%S秒", localtime(&timeInterval), NULL);這這行代碼的調用,strftime_l函數接受四個參數,第一個參數buffer是C語言中字符數組用於存儲日期格式化後的字符串,第二個參數是寫入buffer數組的最大值,如果格式化的字符串大於這個值,那麼只會取字符串的的一部分,第三個參數"%Y年%m月%d日%H時%M分%S秒"是日期格式,第四個參數localtime(&timeInterval)是指向使用當地時區對時間戳處理得到tm類型結構體的指針

附上tm結構體:

struct tm {
    int    tm_sec;        /* seconds after the minute [0-60] */
    int    tm_min;        /* minutes after the hour [0-59] */
    int    tm_hour;    /* hours since midnight [0-23] */
    int    tm_mday;    /* day of the month [1-31] */
    int    tm_mon;        /* months since January [0-11] */
    int    tm_year;    /* years since 1900 */
    int    tm_wday;    /* days since Sunday [0-6] */
    int    tm_yday;    /* days since January 1 [0-365] */
    int    tm_isdst;    /* Daylight Savings Time flag */
    long    tm_gmtoff;    /* offset from UTC in seconds */
    char    *tm_zone;    /* timezone abbreviation */
};

Swift三種情況下的代碼

    //不進行緩存
    func testInOldWay(_ times: Int) {
        var string = ""
        var date = Date.init()
        let startTime = CFAbsoluteTimeGetCurrent();
        for i in 0..<times {
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy年MM月dd日HH時mm分ss秒"
            date = Date.init(timeIntervalSince1970: TimeInterval(1545308405 + i))
            string = formatter.string(from: date)
        }
        let duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
        print("使用oldWay計算\n\(times)次,總耗時\n\(duration) ms\n")
    }
    //進行緩存
    func testInNewWay(_ times: Int) {
        var string = ""
        var date = Date.init()
        let startTime = CFAbsoluteTimeGetCurrent();
        for i in 0..<times {
            date = Date.init(timeIntervalSince1970: TimeInterval(1545308405 + i))
            string = DateFormatterCache.shared.dateFormatterOne.string(from: date)
        }
        let duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
        print("使用緩存Formatter的方案計算\n\(times)次,總耗時\n\(duration) ms\n")
    }
    
    //使用C語言來做日期處理
    func testFormatterInC(_ times: Int) {
        var date = Date.init()
        var dateString = ""
        var buffer = [Int8](repeating: 0, count: 100)
        var time = time_t(date.timeIntervalSince1970)
        let format = "%Y年%m月%d日%H時%M分%S秒"
        let startTime = CFAbsoluteTimeGetCurrent();
        for i in 0..<times {
            date = Date.init(timeIntervalSince1970: TimeInterval(1545308405 + i))
            time = time_t(date.timeIntervalSince1970)
            strftime(&buffer, buffer.count, format, localtime(&time))
            dateString = String.init(cString: buffer, encoding: String.Encoding.utf8) ?? ""
        }
        let duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
        print("使用C語言的方案計算\n\(times)次,總耗時\n\(duration) ms\n")
        print(dateString)
    }

iOS 12.1 iPhone 7

測試結果:

測試結果:

在Objective-C中,
不使用緩存,使用緩存,使用C語言函數處理的耗時比約爲100:10.7:3.5

在Swift中,
不使用緩存,使用緩存,使用C語言函數處理的耗時比約爲100:11.7:6.6

Swift在使用DateFormatter進行處理時,不論是緩存的方案還是不緩存的方案,跟使用Objective-C的耗時基本一致,而在Swift中使用C語言的函數來做日期處理時,時間約爲使用Objective-C的兩倍,而且當只做一次日期處理時,由於涉及到一些初始資源的初始化,所以看上去比後面執行10次的時間還多

最後

如果項目是Objective-C的項目,我覺得可以採用這種C語言的strftime來做日期處理,能將時間降低爲緩存NSDateFormatter的方案的33%左右,如果是Swift項目,調用C語言函數的效率沒有在Objective-C項目中那麼高,雖然能將時間降低爲緩存NSDateFormatter的方案的56%左右,但是在Swift中使用C語言的函數存在一定的風險,在這裏風險之一就是time = time_t(date.timeIntervalSince1970)這行代碼返回的值是time_t類型,time_t類型的定義如下:

public typealias time_t = __darwin_time_t
public typealias __darwin_time_t = Int /* time() */

time_t其實就是Int,當Swift項目運行在32位設備(也就是iphone 5,iphone 5C)上時,Int類型是32位的,最大值爲2147483647,如果這是一個時間戳的值,轉換爲正常時間是2038-01-19 11:14:07,也就是處理的時間是未來的日期,2038年以後的話,會出現數值溢出。

Demo在這裏:
https://github.com/577528249/...

參考資料:

https://forums.developer.appl...
https://stackoverflow.com/que...

PS:

最近加了一些iOS開發相關的QQ羣和微信羣,但是感覺都比較水,裏面對於技術的討論比較少,所以自己建了一個iOS開發進階討論羣,歡迎對技術有熱情的同學掃碼加入,加入以後你可以得到:

1.技術方案的討論,會有在大廠工作的高級開發工程師儘可能抽出時間給大家解答問題

2.每週定期會寫一些文章,並且轉發到羣裏,大家一起討論,也鼓勵加入的同學積極得寫技術文章,提升自己的技術

3.如果有想進大廠的同學,裏面的高級開發工程師也可以給大家內推,並且針對性得給出一些面試建議

羣已經滿100人了,想要加羣的小夥伴們可以掃碼加這個微信,備註:“加羣+暱稱”,拉你進羣,謝謝了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章