NSDateFormatter为什么耗性能
一. 问题背景
很多人都知道NSDateFormatter
频繁创建和使用是一件耗性能的事,很容易引起卡顿问题,因此建议尽量全局使用一个NSDateFormatter
对象。
也有一些文章说NSDateFormatter
最耗性能的stringFromDate
和dateFromString
这两个日期和字符串的转换。
那究竟NSDateFormatter
的性能是损耗在哪里?为什么会引起性能损耗呢?找了一圈也没找到一个比较说服力的解释,因此这里就自己来调研下。
二. NSDateFormatter
的性能是损耗在哪里
1. 首先我们设置三组对照组来算出NSDateFormatter
在循环10000
次下的损耗时间:
第一组:NSDateFormatter
只生成一次,其他属性每次都设置
- (void)testDateFormatterOne {
double startTime = CFAbsoluteTimeGetCurrent();
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
for (int i = 0; i < 100000; i++) {
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
formatter.dateFormat = @"yyyy-MM-dd-HH:mm:ss-zzz";
[formatter stringFromDate:[NSDate date]];
}
[self costTimeWithStartTime:startTime tipStr:@"NSDateFormatter只生成一次,其他属性每次都设置"];
}
第二组: NSDateFormatter只生成一次,其他属性只设置一次
- (void)testDateFormatterSecond {
double startTime = CFAbsoluteTimeGetCurrent();
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
formatter.dateFormat = @"yyyy-MM-dd-HH:mm:ss-zzz";
for (int i = 0; i < 100000; i++) {
[formatter stringFromDate:[NSDate date]];
}
[self costTimeWithStartTime:startTime tipStr:@"NSDateFormatter只生成一次,其他属性只设置一次"];
}
第三组: NSDateFormatter每次都生成
- (void)testDateFormatterThree {
double startTime = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < 100000; i++) {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
formatter.dateFormat = @"yyyy-MM-dd-HH:mm:ss-zzz";
[formatter stringFromDate:[NSDate date]];
}
[self costTimeWithStartTime:startTime tipStr:@"NSDateFormatter每次都生成"];
}
然后执行看所消耗时间:
image.png2022-07-03 21:57:43.467481+0800 FJFBlogProjectDemo[96246:933082] ----------------提示语:NSDateFormatter只生成一次,其他属性每次都设置,且每次进行日期转字符串, costTime: 639.056921 ms
2022-07-03 21:57:43.882939+0800 FJFBlogProjectDemo[96246:933082] ----------------提示语:NSDateFormatter只生成一次,其他属性只设置一次,但每次都进行日期转字符串, costTime: 415.156960 ms
2022-07-03 21:58:07.942717+0800 FJFBlogProjectDemo[96246:933082] ----------------提示语:NSDateFormatter每次都生成,同时设置实例变量属性,并进行日期转换为字符串, costTime: 24059.465051 ms
我们可以看到NSDateFormatter
每次都生成,10000
次调用,消耗了24s
左右,而NSDateFormatter
只生成一次,只消耗1s
不到,很显然每次都生成确实有很大的损耗,那为什么每次都生成NSDateFormatter
变量会有这么大的时间消耗呢?是NSDateFormatter
生成实例变量消耗时间了?还是每次生成NSDateFormatter
的实例变量去加载或者转换消耗了时间呢?
2. 接着我们再次设置三组实现对照组
第三组:NSDateFormatter每次都生成,同时设置实例变量属性,并进行日期转换为字符串
- (void)testDateFormatterThree {
double startTime = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < 100000; i++) {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
formatter.dateFormat = @"yyyy-MM-dd-HH:mm:ss-zzz";
[formatter stringFromDate:[NSDate date]];
}
[self costTimeWithStartTime:startTime tipStr:@"NSDateFormatter每次都生成,同时设置实例变量属性,并进行日期转换为字符串"];
}
第四组: NSDateFormatter每次都生成,但只生成NSDateFormatter实例变量
- (void)testDateFormatterFour {
double startTime = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < 100000; i++) {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
}
[self costTimeWithStartTime:startTime tipStr:@"NSDateFormatter每次都生成,但只生成NSDateFormatter实例变量"];
}
第五组:NSDateFormatter每次都生成,同设置实例变量属性,但不进行日期转换
- (void)testDateFormatterFive {
double startTime = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < 100000; i++) {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
formatter.dateFormat = @"yyyy-MM-dd-HH:mm:ss-zzz";
}
[self costTimeWithStartTime:startTime tipStr:@"NSDateFormatter每次都生成,同设置实例变量属性,但不进行日期转换"];
}
image.png
2022-07-03 22:01:18.779598+0800 FJFBlogProjectDemo[96318:935488] ----------------提示语:NSDateFormatter每次都生成,同时设置实例变量属性,并进行日期转换为字符串, costTime: 22409.832001 ms
2022-07-03 22:01:18.835837+0800 FJFBlogProjectDemo[96318:935488] ----------------提示语:NSDateFormatter每次都生成,但只生成NSDateFormatter实例变量, costTime: 55.896997 ms
2022-07-03 22:01:19.075527+0800 FJFBlogProjectDemo[96318:935488] ----------------提示语:NSDateFormatter每次都生成,同设置实例变量属性,但不进行日期转换, costTime: 239.380956 ms
从这里可以看出NSDateFormatter
生成100000
个实例变量只消耗了55ms
左右,所以NSDateFormatter
生成实例变量是不会造成性能损耗的。
从这三个对照组时间消耗对比中,我们可以看出好像损耗性能最多的就是stringFromDate
这个NSDate
转NSString
的函数,但如果真实这样的话,第一组和第二组实验,也调用了stringFromDate
函数100000
消耗的时间也才几百毫秒。
因此这里可以推断stringFromDate
函数是否损耗性能,还与NSDateFormatter
变量是否为重新生成有关,是否意味着重新生成的NSDateFormatter
实例变量,就代表stringFromDate
每次都要执行耗时加载操作,而如果是缓存的NSDateFormatter
实例变量,则可以调用缓存,不需要执行耗时的加载操作。
3. 基于这个推测,我们再做接下来的三组对照组。
第六组: NSDateFormatter只生成一次,但每次都变化locale,其他属性保持一致,进行日期转换
- (void)testDateFormatterSix {
double startTime = CFAbsoluteTimeGetCurrent();
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
for (int i = 0; i < 100000; i++) {
if (i % 3 == 0) {
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
else if (i % 3 == 1) {
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"zh_CN"];
}
else if (i % 3 == 2) {
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"];
}
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
formatter.dateFormat = @"yyyy-MM-dd-HH:mm:ss-zzz";
[formatter stringFromDate:[NSDate date]];
}
[self costTimeWithStartTime:startTime tipStr:@"NSDateFormatter只生成一次,但每次都变化locale,其他属性保持一致,进行日期转换"];
}
第七组:NSDateFormatter只生成一次,但每次都变化timeZone,其他属性保持一致,进行日期转换
- (void)testDateFormatterSeven {
double startTime = CFAbsoluteTimeGetCurrent();
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
for (int i = 0; i < 100000; i++) {
if (i % 3 == 0) {
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
}
else if (i % 3 == 1) {
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:1];
}
else if (i % 3 == 2) {
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:2];
}
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
formatter.dateFormat = @"yyyy-MM-dd-HH:mm:ss-zzz";
[formatter stringFromDate:[NSDate date]];
}
[self costTimeWithStartTime:startTime tipStr:@"NSDateFormatter只生成一次,但每次都变化timeZone,其他属性保持一致,进行日期转换"];
}
第八组: NSDateFormatter只生成一次,但每次都变化dateFormat,其他属性保持一致,进行日期转换
- (void)testDateFormatterEight {
double startTime = CFAbsoluteTimeGetCurrent();
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
for (int i = 0; i < 100000; i++) {
if (i % 3 == 0) {
formatter.dateFormat = @"yyyy-MM-dd-HH:mm:ss-zzz";
}
else if (i % 3 == 1) {
formatter.dateFormat = @"yyyy-MM-dd-HH:mm:ss";
}
else if (i % 3 == 2) {
formatter.dateFormat = @"yyyy-MM-dd";
}
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
[formatter stringFromDate:[NSDate date]];
}
[self costTimeWithStartTime:startTime tipStr:@"NSDateFormatter只生成一次,但每次都变化dateFormat,其他属性保持一致,进行日期转换"];
}
image.png
2022-07-03 22:25:19.390128+0800 FJFBlogProjectDemo[97133:956816] ----------------提示语:NSDateFormatter只生成一次,但每次都变化locale,其他属性保持一致,进行日期转换, costTime: 19246.747017 ms
2022-07-03 22:25:20.046063+0800 FJFBlogProjectDemo[97133:956816] ----------------提示语:NSDateFormatter只生成一次,但每次都变化timeZone,其他属性保持一致,进行日期转换, costTime: 655.526042 ms
2022-07-03 22:25:20.643665+0800 FJFBlogProjectDemo[97133:956816] ----------------提示语:NSDateFormatter只生成一次,但每次都变化dateFormat,其他属性保持一致,进行日期转换, costTime: 597.255111 ms
从这三个对照组消耗时间,我们可以看出,同样都是只生成一次NSDateFormatter
,每次变化locale
的这组,消耗时间基本跟每次都生成NSDateFormatter
进行日期转换的时间差不多。也就是关键的点就是locale
变化,因此我们要进一步了解下NSLocale
。
4. NSLocale
NSLocale
是一个包含某个地区语言与文化习俗的基础类。一个NSLocale
的实例包含了针对这个地区特定一群人的所有语言文化基准,其中包括语言,键盘,数字,日期和时间格式,货币,排序和分类,符号、颜色与头像使用等。
每一个NSLocale
实例对应着一个地区标识,例如en_US
,fr_FR
,ja_JP
和en_GB
,这些标识包含一个语言码(例如en
代表英语)和一个地区码(例如US
代表美国)
从这个NSLocale
,我们可以推测NSDateFormatter
的实例,在进行stringFromDate
或者dateFromString
方法时,会依据locale
的值,去加载不同地区标识对应的日期格式信息,并缓存,只有当NSDateFormatter
重新初始化或者locale
值做了变更,才会重新取加载地区对应的日期格式显示信息。
基于这个推测,我们来看下gnustep
上面关于NSDateFormatter
的实现逻辑.
5. gnustep
关于NSDateFormatter
的实现
首先我们看下NSDateFormatter
类结构和初始化方法:
从NSDateFormatter
的初始化方法,我们可以看出这里只是简单获取了属性的默认值比如,_behavior
,_locale
,_tz
,_formatter
,所以这里并不会有耗时操作。
接着看下stringFromDate
方法:
从这个方法实现,我推测有可能造成性能损耗的,应该是udat_format
方法,但该方法没有展开看不到内部实现,因此也只是猜测。
然后我们再看下dateFromString
方法的实现:
从这个方法实现,也只能推测真正影响耗时的应该是udat_parse
,同样因为该函数也一样看不到内部展示,因此也只能是推测。
如果大家有更详细的官方资料或者其他验证逻辑,麻烦告知一下。
二. 总结
NSDateFormatter
之所以耗性能是因为NSDateFormatter
的实例进行stringFromDate
或者dateFromString
进行日期与字符串转换时,需要依据NSlocale
去加载不同地区标识相关的日期格式数据,并缓存。
因此有效的对NSDateFormatter
进行优化的方法是依据项目中用到的高频的不同的地区标识,创建多个全局唯一的NSDateFormatter
实例,来进行日期格式和字符串的转换。