背景:最近 BI 的同学反映,根据我们 iOS 客户端收集的埋点数据显示,有一部分数据的埋点时间不对,但是我在工程代码中断点调试时却没有发现任何异常情况。

BI 同学提供的“证据”如下图所示,图中圈出来的 tracktime (00280629104535)本应显示的时间理应是 20160629104535,机智的 BI 同学分析其原因可能是,我们记录时间的代码在某些机器上可能有问题,也就是说机型适配问题,但根据我多年的开发经验来看(不要笑),问题应该不在于此,其中必定另有蹊跷。

埋点数据.png
于是,我看了一下我们记录埋点时间的相关代码:

1
2
3
4
// tt
NSDateFormatter *dateformatter=[[NSDateFormatter alloc] init];
[dateformatter setDateFormat:@"yyyyMMddHHmmss"];
[mutDict setString:[dateformatter stringFromDate:[NSDate date]] forKey:MTCLICK_KEY_TT];

第一眼看上去貌似很正常,打个断点,打印出来的时间也没问题。那么问题在哪呢?难道真是机型问题?但从没听说过机型跟这时间格式有毛关系啊。

那就只能先从 NSDateFormatter 入手了,首先求助于官方文档 Data Formatting Guide,在 Date Formatters 章节中的 Use Format Strings to Specify Custom Formats 发现其中有这样一段话:

There are two things to note about this example:

  1. It uses yyyy to specify the year component. A common mistake is to use YYYY. yyyy specifies the calendar year whereas YYYY specifies the year (of “Week of Year”), used in the ISO year-week calendar. In most cases, yyyy and YYYY yield the same number, however they may be different. Typically you should use the calendar year.

2.The representation of the time may be 13:00. In iOS, however, if the user has switched 24-Hour Time to Off, the time may be 1:00 pm.

读到此处,我感慨万分,到处都是坑啊!顿时想起年初时的那个优惠券有效期的 bug,就是因为时间格式的年份用了 “YYYY”,而不是 “yyyy”,而导致了一个平时看不出来到跨年的时候才出现的问题。

specifies the calendar year whereas YYYY specifies the year (of “Week of Year”), used in the ISO year-week calendar. ```这句话很关键,由此可以看出之前这个问题的根本就在于日历(calendar)的区别。
1
2
3
4
5
6
7

现在我们再回过头来看看这次埋点 bug 的“证据”: **2016**0629104535 -> (**0028**0629104535),问题好像也是出在年份上,那么 0028 代表什么呢?于是我先后求助了度娘、stackoverflow、bing,最终在 bing 上找到了这样一篇文章 [How TechCrunch Japan broke our app - Handling local calendars in Swift](http://blog.famanson.com/2016/01/07/handling-local-calendars-is-a-pain/),里面专门提到了一个关于日本日历(Japan calendar)和公历(Gregorian calendar)之间区别的例子,当我看到这样一段令人感动的话时:
> It means that any NSDate instances retrieved from CoreData would always point to the right point in time, i.e. 04 Jan 0028 in Japan calendar now always points to the same point in time as 04 Jan 2016 in Gregorian calendar (having the same Unix timestamp 1451865600).

原来公历2016年相当于日本平成28年,我顿时感觉豁然开朗,仿佛终于找到心目中的 the one 了。
找到问题所在后,接下来就是试验验证了。
还是那段代码:

NSDateFormatter *dateformatter=[[NSDateFormatter alloc] init];
[dateformatter setDateFormat:@”yyyyMMddHHmmss”];
NSLog(@”%@”, [dateformatter stringFromDate:[NSDate date]]);

1
2
3
打开模拟器的设置->通用->语言与地区->日历,我们依次选择“公历”、“日本日历”、“佛教日历”,并运行工程,console 打印出来的结果是如下。

试验结果说明之前的百分百推断属实。那现在我们该怎么解决这个问题呢?打开 NSDateFormatter.h 文件和参考文档,我们可以看到一个属性 ``@property (null_resettable, copy) NSCalendar *calendar;`` ,然后我们再打开 NSCalendar 类的参考文档,可以看到 ``Calendar Identifiers``常量的一些声明:

NSString const NSCalendarIdentifierGregorian
NSString
const NSCalendarIdentifierBuddhist
NSString const NSCalendarIdentifierChinese
NSString
const NSCalendarIdentifierCoptic
NSString const NSCalendarIdentifierEthiopicAmeteMihret
NSString
const NSCalendarIdentifierEthiopicAmeteAlem
NSString const NSCalendarIdentifierHebrew
NSString
const NSCalendarIdentifierISO8601
NSString const NSCalendarIdentifierIndian
NSString
const NSCalendarIdentifierIslamic
NSString const NSCalendarIdentifierIslamicCivil
NSString
const NSCalendarIdentifierJapanese
NSString const NSCalendarIdentifierPersian
NSString
const NSCalendarIdentifierRepublicOfChina
NSString const NSCalendarIdentifierIslamicTabular
NSString
const NSCalendarIdentifierIslamicUmmAlQura

1
显而易见的是,这里我们应该选择 NSCalendarIdentifierGregorian(公历)。

NSDateFormatter *dateformatter=[[NSDateFormatter alloc] init];
[dateformatter setDateFormat:@”yyyyMMddHHmmss”];
[dateformatter setCalendar:[NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]];
NSLog(@”%@”, [dateformatter stringFromDate:[NSDate date]]);
```
重新修改代码,再次验证后结果如下。

公历(Gregorian).png

日本日历(Japanese).png

佛教日历(Buddhist).png

设置选项中为日本日历(Japanese),但代码中设了 calendar 属性.png

试验结果证明设置 NSDateFormatter 对象的 calendar 属性为 identifier 为公历NSCalendarIdentifierGregorian的 NSCalendar 对象就可以解决这个问题了。

NSDateFormatter 是我们经常需要使用的一个类,但在使用过程中需要注意很多有关地区,时间,性能方面的问题,要想知道如何一一避免那些坑,多读官方文档是一个上佳的选择。

One more thing,人蠢就要多读书啦!不然连日本日历是什么都不知道是什么。Just kidding! 😄

参考资料:
(1)How TechCrunch Japan broke our app - Handling local calendars in Swift:http://blog.famanson.com/2016/01/07/handling-local-calendars-is-a-pain/
(2)官方文档: Data Formatting Guide


拓展延伸:

(1)日历有哪几种?什么是日本日历?什么是佛教日历?
(2)如何正确使用 NSDateFormatter ?