为什么写这篇文章
1.之前在一些性能优化的文章中,看到有提到“创建DateFormatter开销会比较大”,也有的文章里面说是“设置日期格式”这个方法较为耗时,但实际上测试发现是“生成字符串”这个方法较为耗时,所以我觉得可以纠正一些这些说法
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..
日志输出
使用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..
输出结果:
执行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在这里
PS:
最近加了一些iOS开发相关的QQ群和微信群,但是感觉都比较水,里面对于技术的讨论比较少,所以自己建了一个iOS开发进阶讨论群,欢迎对技术有热情的同学扫码加入,加入以后你可以得到:
1.技术方案的讨论,会有在大厂工作的高级开发工程师尽可能抽出时间给大家解答问题
2.每周定期会写一些文章,并且转发到群里,大家一起讨论,也鼓励加入的同学积极得写技术文章,提升自己的技术
3.如果有想进大厂的同学,里面的高级开发工程师也可以给大家内推,并且针对性得给出一些面试建议
群已经满100人了,想要加群的小伙伴们可以扫码加这个微信,备注:“加群+昵称”,拉你进群,谢谢了