[性能優化]DateFormatter輕度優化探索

爲何寫這篇文章

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

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

2.不少同窗可能只是跟我以前同樣,只是知道這個方法比較耗時,可是對於進行緩存優化後的效果對比並不清楚,因此本身寫了一個小Demo,對優化先後進行一些性能測試,方便你們參考,也方便你們在項目中使用。ios

運行時間對比

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

執行時間統計:
git

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

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,因此也說明了生成日期字符串的方法耗時較多。app

在項目中的實際提高

測試環境:iPhone 7ide

測試系統: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人了,想要加羣的小夥伴們能夠掃碼加這個微信,備註:「加羣+暱稱」,拉你進羣,謝謝了

相關文章
相關標籤/搜索