DateFormatter輕度優化探索

爲什麼寫這篇文章

1.之前在一些性能優化的文章《性能優化之NSDateFormatter》中,看到有提到“創建DateFormatter開銷會比較大”,也有的文章《(多帖總結) iOS性能優化技巧》裏面說是“設置日期格式”這個方法較爲耗時,但實際上測試發現是“生成字符串”這個方法較爲耗時,所以我覺得可以糾正一些這些說法

let formatter = DateFormatter()//創建DateFormatter實例對象
formatter.dateFormat = "yyyy年MM月dd日"//設置日期格式
string = formatter.string(from: date)//生成字符串

2.很多同學可能只是跟我之前一樣,只是知道這個方法比較耗時,但是對於進行緩存優化後的效果對比並不清楚,所以自己寫了一個小Demo,對優化前後進行一些性能測試,方便大家參考,也方便大家在項目中使用。

運行時間對比

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        testInOldWay(1)
        testInNewWay(1)
        
        testInOldWay(10)
        testInNewWay(10)
        
        testInOldWay(100)
        testInNewWay(100)
        
        testInOldWay(1000)
        testInNewWay(1000)
        
        testInOldWay(10000)
        testInNewWay(10000)
        
        testInOldWay(100000)
        testInNewWay(100000)
        
        testInOldWay(1000000)
        testInNewWay(1000000)
    }
    //不進行緩存
    func testInOldWay(_ times: Int) {
        var string = ""
        let date = Date.init()
        let startTime = CFAbsoluteTimeGetCurrent();
        for _ in 0..<times {
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy年MM月dd日"
            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 = ""
        let date = Date.init()
        let startTime = CFAbsoluteTimeGetCurrent();
        for _ in 0..<times {
            string = DateFormatterCache.shared.dateFormatterOne.string(from: date)
        }
        let duration = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0;
        print("使用newWay計算\n\(times)次,總耗時\n\(duration) ms\n")
    }
}


//創建單例進行緩存
class DateFormatterCache {
    //使用方法
    //let timeStr = DateFormatterCache.shared.dateFormatterOne.string(from: publishTime)
    static let shared = DateFormatterCache.init()
    
    lazy var dateFormatterOne: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy年MM月dd日"
        return formatter
    }()
    lazy var dateFormatterTwo: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .full
        formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss z"
        formatter.locale = Locale.init(identifier: "en_US")
        return formatter
    }()
}

日誌輸出

使用oldWay計算
1次,總耗時
7.187008857727051 ms

使用newWay計算
1次,總耗時
0.1609325408935547 ms

使用oldWay計算
10次,總耗時
0.552058219909668 ms

使用newWay計算
10次,總耗時
0.05888938903808594 ms

使用oldWay計算
100次,總耗時
4.320979118347168 ms

使用newWay計算
100次,總耗時
0.6080865859985352 ms

使用oldWay計算
1000次,總耗時
47.60599136352539 ms

使用newWay計算
1000次,總耗時
5.526900291442871 ms

使用oldWay計算
10000次,總耗時
427.8249740600586 ms

使用newWay計算
10000次,總耗時
45.81403732299805 ms

使用oldWay計算
100000次,總耗時
4123.620986938477 ms

使用newWay計算
100000次,總耗時
459.98501777648926 ms

使用oldWay計算
1000000次,總耗時
40522.77398109436 ms

使用newWay計算
1000000次,總耗時
4625.54395198822 ms

執行時間統計:

在測試中,我們發現執行一次formatter的創建和設置日期格式需要7.187008857727051 ms,執行10次卻只需要0.552058219909668 ms,這是因爲第一次執行let formatter = DateFormatter()這行代碼時可能會涉及到DateFormatter類相關的一些初始資源的初始化,而後續執行十次時已經不包含這一過程所需要的耗時,所以看上去執行一次的時間反而長一些,我們在計算性能比較時可以通過增加執行次數,來忽略這些因素的影響,當我們執行1000000次時,不進行緩存使用oldWay計算需要40522.77398109436 ms,而一次初始化的開銷最大爲第一次的執行的耗時7.187008857727051 ms,

7.18/40522.77 = 0.0177%

時間佔比爲0.0177,這些因素的影響已經降低爲萬分之一了,所以我們可以將執行1000000次時,不使用緩存和使用緩存的執行一次所需平均時間方法耗時

不使用緩存(oldWay,每次創建DateFormatter對象並且設置格式)
執行一次耗時:40.52 us
使用緩存(oldWay,每次創建DateFormatter對象並且設置格式)
執行一次耗時:4.625 us

使用緩存的方案的執行時間大概是不使用緩存的方案的時間的11.4%

究竟是創建DateFormatter對象耗時還是設置日期格式耗時呢?

 func testPartInOldWay(_ times: Int) {
        var string = ""
        let date = Date.init()
        var startTime1: CFAbsoluteTime = 0;
        var startTime2: CFAbsoluteTime = 0;
        var startTime3: CFAbsoluteTime = 0;
        var startTime4: CFAbsoluteTime = 0;

        var duration1: CFAbsoluteTime = 0;
        var duration2: CFAbsoluteTime = 0;
        var duration3: CFAbsoluteTime = 0;

        for i in 0..<times {
            startTime1 = CFAbsoluteTimeGetCurrent();
            let formatter = DateFormatter()
            startTime2 = CFAbsoluteTimeGetCurrent();
            formatter.dateFormat = "yyyy年MM月dd日"
            startTime3 = CFAbsoluteTimeGetCurrent();
            string = formatter.string(from: date)
            startTime4 = CFAbsoluteTimeGetCurrent();
            
            duration1 += (startTime2 - startTime1) * 1000.0;
            duration2 += (startTime3 - startTime2) * 1000.0;
            duration3 += (startTime4 - startTime3) * 1000.0;
        }
        print("創建DateFormatter對象耗時=\(duration1)ms\n設置日期格式耗時=\(duration2)ms\n生成字符串耗時=\(duration3)ms\n\n")
    }

輸出結果:

執行1次
創建DateFormatter對象耗時=0.030994415283203125ms
設置日期格式耗時=0.3859996795654297ms
生成字符串耗時=1.6570091247558594ms

執行10次
創建DateFormatter對象耗時=0.019073486328125ms
設置日期格式耗時=0.012159347534179688ms
生成字符串耗時=0.5759000778198242ms

執行100次
創建DateFormatter對象耗時=0.0768899917602539ms
設置日期格式耗時=0.06973743438720703ms
生成字符串耗時=4.322528839111328ms

執行1000次
創建DateFormatter對象耗時=0.7123947143554688ms
設置日期格式耗時=0.702977180480957ms
生成字符串耗時=41.77117347717285ms

執行10000次
創建DateFormatter對象耗時=6.549596786499023ms
設置日期格式耗時=5.913138389587402ms
生成字符串耗時=370.6216812133789ms

執行100000次
創建DateFormatter對象耗時=65.13833999633789ms
設置日期格式耗時=59.78119373321533ms
生成字符串耗時=3586.0002040863037ms

執行1000000次
創建DateFormatter對象耗時=661.7592573165894ms
設置日期格式耗時=575.5696296691895ms
生成字符串耗時=35309.07988548279ms

可以從輸出結果中發現是string = formatter.string(from: date)這行代碼耗費時間最多,所以主要耗時並不在於執行DateFormatter.init()和formatter.dateFormat = "yyyy年MM月dd日",在對我們項目使用Instrument進行分析時,測試結果也證明了這一點

測試環境:iPhone 7

測試系統:iOS 12.1(16B92)

app啓動後的60s內,快速滑動feed流頁面,在這一過程中,主線程的執行時間大概是10.59s,我們項目中日期處理主要在func detailString(date: Date) -> String這個方法中進行,這個方法的運行時間爲730ms,而其中 timeStr = formatter.string(from: date)這行代碼的運行時間爲628ms,所以也說明了生成日期字符串的方法耗時較多。

在項目中的實際提升

測試環境:iPhone 7

測試系統:iOS 12.1(16B92)

測試時間:app啓動後的60s

測試步驟:使用Instruments的Time Profiler啓動app,在啓動後的60s內,快速滑動列表頁。

沒有對DateFormatter進行緩存時:

在我們項目中,detailString方法每次調用時會創建DateFormatter,生成日期字符串

let formatter = DateFormatter()
formatter.dateFormat = "MM月dd日"
timeStr = formatter.string(from: date)

測試結果:

app啓動後的60s內,主線程執行時間10.59s,detailString的執行730ms

對DateFormatter進行緩存後:

    timeStr = DateFormatterCache.shared.dateFormatterOne.string(from: date)
    class DateFormatterCache {
        //使用方法
        //let timeStr = DateFormatterCache.shared.dateFormatterOne.string(from: publishTime)
        static let shared = DateFormatterCache.init()
        
        lazy var dateFormatterOne: DateFormatter = {
            let formatter = DateFormatter()
            formatter.dateFormat = "MM月dd日"
            return formatter
    }()

我們通過DateFormatterCache的單例對象shared來獲取dateFormatterOne

測試結果:


app啓動後的60s內,主線程執行時間10.58s,detailString的執行76ms

從執行時間上對比,緩存後,執行時間是之前的10.4%,對性能的提升還是比較大的

最後

因爲系統內部的實現,我們看不到源碼,我在私下針對DateFormatter的創建,設置日期格式,生成字符串三個步驟分別做過大量測試,但是也有可能是測試方法的侷限性(是通過統計每個步驟調用時間來彙總的,沒法通過調用一百萬次方法來計算總時間來統計的),暫時來說無法分析出具體是哪一步驟是主要耗時的,但是在項目中,如果使用單例來對創建,設置日期格式這兩個步驟來緩存,使用Instrument進行分析時確實可以將運行時間降爲不緩存時的10%左右。

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

PS:

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

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

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

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

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

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