[性能优化]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人了,想要加群的小伙伴们能够扫码加这个微信,备注:“加群+昵称”,拉你进群,谢谢了