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实例,来进行日期格式和字符串的转换。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容