NSDateFormatter为什么耗性能

一. 问题背景

很多人都知道NSDateFormatter频繁创建和使用是一件耗性能的事,很容易引起卡顿问题,因此建议尽量全局使用一个NSDateFormatter对象。

也有一些文章说NSDateFormatter最耗性能的stringFromDatedateFromString这两个日期和字符串的转换。

那究竟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.png
2022-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这个NSDateNSString的函数,但如果真实这样的话,第一组和第二组实验,也调用了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_USfr_FRja_JPen_GB,这些标识包含一个语言码(例如en代表英语)和一个地区码(例如US代表美国)

从这个NSLocale,我们可以推测NSDateFormatter的实例,在进行stringFromDate或者dateFromString方法时,会依据locale的值,去加载不同地区标识对应的日期格式信息,并缓存,只有当NSDateFormatter重新初始化或者locale值做了变更,才会重新取加载地区对应的日期格式显示信息。

基于这个推测,我们来看下gnustep上面关于NSDateFormatter的实现逻辑.

5. gnustep关于NSDateFormatter的实现

首先我们看下NSDateFormatter类结构和初始化方法:

image.png
image.png
image.png

NSDateFormatter的初始化方法,我们可以看出这里只是简单获取了属性的默认值比如,_behavior_locale_tz_formatter,所以这里并不会有耗时操作。

接着看下stringFromDate方法:

image.png

从这个方法实现,我推测有可能造成性能损耗的,应该是udat_format方法,但该方法没有展开看不到内部实现,因此也只是猜测。

然后我们再看下dateFromString方法的实现:

image.png

从这个方法实现,也只能推测真正影响耗时的应该是udat_parse,同样因为该函数也一样看不到内部展示,因此也只能是推测。

如果大家有更详细的官方资料或者其他验证逻辑,麻烦告知一下。

二. 总结

NSDateFormatter之所以耗性能是因为NSDateFormatter的实例进行stringFromDate或者dateFromString进行日期与字符串转换时,需要依据NSlocale去加载不同地区标识相关的日期格式数据,并缓存。

因此有效的对NSDateFormatter进行优化的方法是依据项目中用到的高频的不同的地区标识,创建多个全局唯一的NSDateFormatter实例,来进行日期格式和字符串的转换。

你可能感兴趣的:(NSDateFormatter为什么耗性能)