一. 問題背景
很多人都知道NSDateFormatter
頻繁創(chuàng)建和使用是一件耗性能的事构舟,很容易引起卡頓問題呛凶,因此建議盡量全局使用一個(gè)NSDateFormatter
對象病往。
也有一些文章說NSDateFormatter
最耗性能的stringFromDate
和dateFromString
這兩個(gè)日期和字符串的轉(zhuǎn)換举塔。
那究竟NSDateFormatter
的性能是損耗在哪里?為什么會(huì)引起性能損耗呢奉狈?找了一圈也沒找到一個(gè)比較說服力的解釋,因此這里就自己來調(diào)研下涩惑。
二. NSDateFormatter
的性能是損耗在哪里
1. 首先我們設(shè)置三組對照組來算出NSDateFormatter
在循環(huán)10000
次下的損耗時(shí)間:
第一組:NSDateFormatter
只生成一次仁期,其他屬性每次都設(shè)置
- (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只生成一次,其他屬性每次都設(shè)置"];
}
第二組: NSDateFormatter只生成一次,其他屬性只設(shè)置一次
- (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只生成一次跛蛋,其他屬性只設(shè)置一次"];
}
第三組: 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每次都生成"];
}
然后執(zhí)行看所消耗時(shí)間:
2022-07-03 21:57:43.467481+0800 FJFBlogProjectDemo[96246:933082] ----------------提示語:NSDateFormatter只生成一次熬的,其他屬性每次都設(shè)置,且每次進(jìn)行日期轉(zhuǎn)字符串, costTime: 639.056921 ms
2022-07-03 21:57:43.882939+0800 FJFBlogProjectDemo[96246:933082] ----------------提示語:NSDateFormatter只生成一次,其他屬性只設(shè)置一次赊级,但每次都進(jìn)行日期轉(zhuǎn)字符串, costTime: 415.156960 ms
2022-07-03 21:58:07.942717+0800 FJFBlogProjectDemo[96246:933082] ----------------提示語:NSDateFormatter每次都生成,同時(shí)設(shè)置實(shí)例變量屬性押框,并進(jìn)行日期轉(zhuǎn)換為字符串, costTime: 24059.465051 ms
我們可以看到NSDateFormatter
每次都生成,10000
次調(diào)用理逊,消耗了24s
左右橡伞,而NSDateFormatter
只生成一次,只消耗1s
不到晋被,很顯然每次都生成確實(shí)有很大的損耗兑徘,那為什么每次都生成NSDateFormatter
變量會(huì)有這么大的時(shí)間消耗呢?是NSDateFormatter
生成實(shí)例變量消耗時(shí)間了墨微?還是每次生成NSDateFormatter
的實(shí)例變量去加載或者轉(zhuǎn)換消耗了時(shí)間呢道媚?
2. 接著我們再次設(shè)置三組實(shí)現(xiàn)對照組
第三組:NSDateFormatter每次都生成,同時(shí)設(shè)置實(shí)例變量屬性,并進(jìn)行日期轉(zhuǎn)換為字符串
- (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每次都生成,同時(shí)設(shè)置實(shí)例變量屬性翘县,并進(jìn)行日期轉(zhuǎn)換為字符串"];
}
第四組: NSDateFormatter每次都生成,但只生成NSDateFormatter實(shí)例變量
- (void)testDateFormatterFour {
double startTime = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < 100000; i++) {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
}
[self costTimeWithStartTime:startTime tipStr:@"NSDateFormatter每次都生成,但只生成NSDateFormatter實(shí)例變量"];
}
第五組:NSDateFormatter每次都生成最域,同設(shè)置實(shí)例變量屬性,但不進(jìn)行日期轉(zhuǎn)換
- (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每次都生成锈麸,同設(shè)置實(shí)例變量屬性镀脂,但不進(jìn)行日期轉(zhuǎn)換"];
}
2022-07-03 22:01:18.779598+0800 FJFBlogProjectDemo[96318:935488] ----------------提示語:NSDateFormatter每次都生成,同時(shí)設(shè)置實(shí)例變量屬性,并進(jìn)行日期轉(zhuǎn)換為字符串, costTime: 22409.832001 ms
2022-07-03 22:01:18.835837+0800 FJFBlogProjectDemo[96318:935488] ----------------提示語:NSDateFormatter每次都生成,但只生成NSDateFormatter實(shí)例變量, costTime: 55.896997 ms
2022-07-03 22:01:19.075527+0800 FJFBlogProjectDemo[96318:935488] ----------------提示語:NSDateFormatter每次都生成忘伞,同設(shè)置實(shí)例變量屬性薄翅,但不進(jìn)行日期轉(zhuǎn)換, costTime: 239.380956 ms
從這里可以看出NSDateFormatter
生成100000
個(gè)實(shí)例變量只消耗了55ms
左右,所以NSDateFormatter
生成實(shí)例變量是不會(huì)造成性能損耗的氓奈。
從這三個(gè)對照組時(shí)間消耗對比中翘魄,我們可以看出好像損耗性能最多的就是stringFromDate
這個(gè)NSDate
轉(zhuǎn)NSString
的函數(shù),但如果真實(shí)這樣的話舀奶,第一組和第二組實(shí)驗(yàn)暑竟,也調(diào)用了stringFromDate
函數(shù)100000
消耗的時(shí)間也才幾百毫秒。
因此這里可以推斷stringFromDate
函數(shù)是否損耗性能育勺,還與NSDateFormatter
變量是否為重新生成有關(guān)但荤,是否意味著重新生成的NSDateFormatter
實(shí)例變量,就代表stringFromDate
每次都要執(zhí)行耗時(shí)加載操作涧至,而如果是緩存的NSDateFormatter
實(shí)例變量腹躁,則可以調(diào)用緩存,不需要執(zhí)行耗時(shí)的加載操作南蓬。
3. 基于這個(gè)推測纺非,我們再做接下來的三組對照組哑了。
第六組: NSDateFormatter只生成一次,但每次都變化locale铐炫,其他屬性保持一致垒手,進(jìn)行日期轉(zhuǎn)換
- (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倒信,其他屬性保持一致科贬,進(jìn)行日期轉(zhuǎn)換"];
}
第七組:NSDateFormatter只生成一次,但每次都變化timeZone鳖悠,其他屬性保持一致,進(jìn)行日期轉(zhuǎn)換
- (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,其他屬性保持一致,進(jìn)行日期轉(zhuǎn)換"];
}
第八組: NSDateFormatter只生成一次乘综,但每次都變化dateFormat憎账,其他屬性保持一致,進(jìn)行日期轉(zhuǎn)換
- (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卡辰,其他屬性保持一致,進(jìn)行日期轉(zhuǎn)換"];
}
2022-07-03 22:25:19.390128+0800 FJFBlogProjectDemo[97133:956816] ----------------提示語:NSDateFormatter只生成一次胞皱,但每次都變化locale,其他屬性保持一致九妈,進(jìn)行日期轉(zhuǎn)換, costTime: 19246.747017 ms
2022-07-03 22:25:20.046063+0800 FJFBlogProjectDemo[97133:956816] ----------------提示語:NSDateFormatter只生成一次反砌,但每次都變化timeZone,其他屬性保持一致,進(jìn)行日期轉(zhuǎn)換, costTime: 655.526042 ms
2022-07-03 22:25:20.643665+0800 FJFBlogProjectDemo[97133:956816] ----------------提示語:NSDateFormatter只生成一次萌朱,但每次都變化dateFormat宴树,其他屬性保持一致,進(jìn)行日期轉(zhuǎn)換, costTime: 597.255111 ms
從這三個(gè)對照組消耗時(shí)間,我們可以看出晶疼,同樣都是只生成一次NSDateFormatter
酒贬,每次變化locale
的這組,消耗時(shí)間基本跟每次都生成NSDateFormatter
進(jìn)行日期轉(zhuǎn)換的時(shí)間差不多翠霍。也就是關(guān)鍵的點(diǎn)就是locale
變化锭吨,因此我們要進(jìn)一步了解下NSLocale
。
4. NSLocale
NSLocale
是一個(gè)包含某個(gè)地區(qū)語言與文化習(xí)俗的基礎(chǔ)類寒匙。一個(gè)NSLocale
的實(shí)例包含了針對這個(gè)地區(qū)特定一群人的所有語言文化基準(zhǔn)耐齐,其中包括語言,鍵盤蒋情,數(shù)字,日期和時(shí)間格式耸携,貨幣棵癣,排序和分類,符號(hào)夺衍、顏色與頭像使用等狈谊。
每一個(gè)NSLocale
實(shí)例對應(yīng)著一個(gè)地區(qū)標(biāo)識(shí),例如en_US
,fr_FR
河劝,ja_JP
和en_GB
壁榕,這些標(biāo)識(shí)包含一個(gè)語言碼(例如en
代表英語)和一個(gè)地區(qū)碼(例如US
代表美國)
從這個(gè)NSLocale
,我們可以推測NSDateFormatter
的實(shí)例,在進(jìn)行stringFromDate
或者dateFromString
方法時(shí)赎瞎,會(huì)依據(jù)locale
的值牌里,去加載不同地區(qū)標(biāo)識(shí)對應(yīng)的日期格式信息,并緩存务甥,只有當(dāng)NSDateFormatter
重新初始化或者locale
值做了變更牡辽,才會(huì)重新取加載地區(qū)對應(yīng)的日期格式顯示信息。
基于這個(gè)推測敞临,我們來看下gnustep
上面關(guān)于NSDateFormatter
的實(shí)現(xiàn)邏輯.
5. gnustep
關(guān)于NSDateFormatter
的實(shí)現(xiàn)
首先我們看下NSDateFormatter
類結(jié)構(gòu)和初始化方法:
從NSDateFormatter
的初始化方法态辛,我們可以看出這里只是簡單獲取了屬性的默認(rèn)值比如,_behavior
挺尿,_locale
奏黑,_tz
,_formatter
编矾,所以這里并不會(huì)有耗時(shí)操作熟史。
接著看下stringFromDate
方法:
從這個(gè)方法實(shí)現(xiàn),我推測有可能造成性能損耗的洽沟,應(yīng)該是udat_format
方法以故,但該方法沒有展開看不到內(nèi)部實(shí)現(xiàn),因此也只是猜測裆操。
然后我們再看下dateFromString
方法的實(shí)現(xiàn):
從這個(gè)方法實(shí)現(xiàn)怒详,也只能推測真正影響耗時(shí)的應(yīng)該是udat_parse
,同樣因?yàn)樵摵瘮?shù)也一樣看不到內(nèi)部展示,因此也只能是推測踪区。
如果大家有更詳細(xì)的官方資料或者其他驗(yàn)證邏輯昆烁,麻煩告知一下。
三. 總結(jié)
NSDateFormatter
之所以耗性能是因?yàn)?code>NSDateFormatter的實(shí)例進(jìn)行stringFromDate
或者dateFromString
進(jìn)行日期與字符串轉(zhuǎn)換時(shí)缎岗,需要依據(jù)NSlocale
去加載不同地區(qū)標(biāo)識(shí)相關(guān)的日期格式數(shù)據(jù)静尼,并緩存。
因此有效的對NSDateFormatter
進(jìn)行優(yōu)化的方法是依據(jù)項(xiàng)目中用到的高頻的不同的地區(qū)標(biāo)識(shí)传泊,創(chuàng)建多個(gè)全局唯一的NSDateFormatter
實(shí)例鼠渺,來進(jìn)行日期格式和字符串的轉(zhuǎn)換。