正确的 NSCalendar 初始化
Proper NSCalendar initialization
我们的应用程序在日期方面工作得很好,但目前我们只支持公历,并且应用程序范围内的 NSCalendar 实例初始化如下:
NSCalendar *appCalendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
上述方法的文档指出 "The returned calendar defaults to the current locale and default time zone." 但是,当 运行 在区域设置为 "United Kingdom" 的设备上运行应用程序时,调用 [appCalendar firstWeekday]
returned 值 1(星期日)而不是预期的 2(星期一)。如果我运行[[NSCalendar currentCalendar] firstWeekday]
,2的正确值是returned。起初我认为 "appCalendar" 上可能没有设置区域设置,但日志记录显示它有一个区域设置,尽管它缺少国家代码等,"currentCalendar" 实例确实有并且允许它 return 正确的 firstWeekDay。
是否应该在从 calendarWithIdentifier
编辑的 return 对象上显式设置语言环境?如果是,这样做有什么注意事项吗?
更新
根据下面 zrzka 的回答,我建议在使用标识符初始化日历时明确设置语言环境,例如
NSCalendar *appCalendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
appCalendar.locale = [NSLocale currentLocale];
The returned calendar defaults to the current locale and default time zone.
应该是:
The returned calendar defaults to the system locale and default time zone.
CFCalendarRef CFCalendarCreateWithIdentifier(CFAllocatorRef allocator, CFStringRef identifier) {
if (allocator == NULL) allocator = __CFGetDefaultAllocator();
__CFGenericValidateType(allocator, CFAllocatorGetTypeID());
__CFGenericValidateType(identifier, CFStringGetTypeID());
// return NULL until Chinese calendar is available
if (identifier != kCFGregorianCalendar && identifier != kCFBuddhistCalendar && identifier != kCFJapaneseCalendar && identifier != kCFIslamicCalendar && identifier != kCFIslamicCivilCalendar && identifier != kCFHebrewCalendar) {
// if (identifier != kCFGregorianCalendar && identifier != kCFBuddhistCalendar && identifier != kCFJapaneseCalendar && identifier != kCFIslamicCalendar && identifier != kCFIslamicCivilCalendar && identifier != kCFHebrewCalendar && identifier != kCFChineseCalendar) {
if (CFEqual(kCFGregorianCalendar, identifier)) identifier = kCFGregorianCalendar;
else if (CFEqual(kCFBuddhistCalendar, identifier)) identifier = kCFBuddhistCalendar;
else if (CFEqual(kCFJapaneseCalendar, identifier)) identifier = kCFJapaneseCalendar;
else if (CFEqual(kCFIslamicCalendar, identifier)) identifier = kCFIslamicCalendar;
else if (CFEqual(kCFIslamicCivilCalendar, identifier)) identifier = kCFIslamicCivilCalendar;
else if (CFEqual(kCFHebrewCalendar, identifier)) identifier = kCFHebrewCalendar;
// else if (CFEqual(kCFChineseCalendar, identifier)) identifier = kCFChineseCalendar;
else return NULL;
}
struct __CFCalendar *calendar = NULL;
uint32_t size = sizeof(struct __CFCalendar) - sizeof(CFRuntimeBase);
calendar = (struct __CFCalendar *)_CFRuntimeCreateInstance(allocator, CFCalendarGetTypeID(), size, NULL);
if (NULL == calendar) {
return NULL;
}
calendar->_identifier = (CFStringRef)CFRetain(identifier);
calendar->_locale = NULL;
calendar->_localeID = CFLocaleGetIdentifier(CFLocaleGetSystem());
calendar->_tz = CFTimeZoneCopyDefault();
calendar->_cal = NULL;
return (CFCalendarRef)calendar;
}
_locale
用 NULL
初始化,_localeID
用系统区域设置的区域设置标识符初始化(在 iPhone 和模拟器上是一个空字符串)。 _cal
设置为 NULL
。
CFIndex CFCalendarGetFirstWeekday(CFCalendarRef calendar) {
CF_OBJC_FUNCDISPATCHV(CFCalendarGetTypeID(), CFIndex, calendar, firstWeekday);
__CFGenericValidateType(calendar, CFCalendarGetTypeID());
if (!calendar->_cal) __CFCalendarSetupCal(calendar);
if (calendar->_cal) {
return ucal_getAttribute(calendar->_cal, UCAL_FIRST_DAY_OF_WEEK);
}
return -1;
}
因此,因为 _cal
是 NULL
,所以调用了 __CFCalendarSetupCal
。
static void __CFCalendarSetupCal(CFCalendarRef calendar) {
calendar->_cal = __CFCalendarCreateUCalendar(calendar->_identifier, calendar->_localeID, calendar->_tz);
}
用空字符串 _localeID
调用 __CFCalendarCreateUCalendar
。
我可以在 iOS 11、12 和 13 上确认此行为。源代码是针对名为 CF-Lite 的东西,但我进一步反汇编了实际的CoreFoundation 框架和它做同样的事情...
call _CFLocaleGetSystem ; _CFLocaleGetSystem
mov rdi, rax ; argument "cf" for method _CFRetain
call _CFRetain ; _CFRetain
mov qword [r15+0x18], rax
call _CFTimeZoneCopyDefault ; _CFTimeZoneCopyDefault
mov qword [r15+0x20], rax
mov rbx, qword [r15+0x10]
mov rdi, qword [r15+0x18] ; argument "locale" for method _CFLocaleGetIdentifier
call _CFLocaleGetIdentifier ; _CFLocaleGetIdentifier
mov rdx, qword [r15+0x20] ; argument #3 for method ___CFCalendarCreateUCalendar
mov rdi, rbx ; argument #1 for method ___CFCalendarCreateUCalendar
mov rsi, rax ; argument #2 for method ___CFCalendarCreateUCalendar
call ___CFCalendarCreateUCalendar ; ___CFCalendarCreateUCalendar
... 使用来自 CFLocaleGetSystem
的 CFLocaleGetIdentifier
的空标识符。
当您查看 CFCalendarCreateWithIdentifier
文档时,没有关于当前语言环境、时区、......
更有趣的是这两种方法的区别(讨论部分):
+calendarWithIdentifier:
- 它包含有关当前语言环境的信息,...
-initWithCalendarIdentifier:
- 只字未提当前语言环境,...
但是没有区别,calendarWithIdentifier:
只是调用了alloc
& initWithCalendarIdentifier:
.
push rbp
mov rbp, rsp
push r14
push rbx
mov rbx, rdx
mov rsi, qword [0x3cb478] ; argument "selector" for method _objc_msgSend, @selector(alloc)
mov r14, qword [_objc_msgSend_390220] ; _objc_msgSend_390220
call r14 ; Jumps to 0x553ae0 (_objc_msgSend), _objc_msgSend
mov rsi, qword [0x3cc768] ; argument "selector" for method _objc_msgSend, @selector(initWithCalendarIdentifier:)
...
我认为这是一个文档问题,应该报告给 Apple(是吗,FB7740798)。
我们的应用程序在日期方面工作得很好,但目前我们只支持公历,并且应用程序范围内的 NSCalendar 实例初始化如下:
NSCalendar *appCalendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
上述方法的文档指出 "The returned calendar defaults to the current locale and default time zone." 但是,当 运行 在区域设置为 "United Kingdom" 的设备上运行应用程序时,调用 [appCalendar firstWeekday]
returned 值 1(星期日)而不是预期的 2(星期一)。如果我运行[[NSCalendar currentCalendar] firstWeekday]
,2的正确值是returned。起初我认为 "appCalendar" 上可能没有设置区域设置,但日志记录显示它有一个区域设置,尽管它缺少国家代码等,"currentCalendar" 实例确实有并且允许它 return 正确的 firstWeekDay。
是否应该在从 calendarWithIdentifier
编辑的 return 对象上显式设置语言环境?如果是,这样做有什么注意事项吗?
更新
根据下面 zrzka 的回答,我建议在使用标识符初始化日历时明确设置语言环境,例如
NSCalendar *appCalendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
appCalendar.locale = [NSLocale currentLocale];
The returned calendar defaults to the current locale and default time zone.
应该是:
The returned calendar defaults to the system locale and default time zone.
CFCalendarRef CFCalendarCreateWithIdentifier(CFAllocatorRef allocator, CFStringRef identifier) {
if (allocator == NULL) allocator = __CFGetDefaultAllocator();
__CFGenericValidateType(allocator, CFAllocatorGetTypeID());
__CFGenericValidateType(identifier, CFStringGetTypeID());
// return NULL until Chinese calendar is available
if (identifier != kCFGregorianCalendar && identifier != kCFBuddhistCalendar && identifier != kCFJapaneseCalendar && identifier != kCFIslamicCalendar && identifier != kCFIslamicCivilCalendar && identifier != kCFHebrewCalendar) {
// if (identifier != kCFGregorianCalendar && identifier != kCFBuddhistCalendar && identifier != kCFJapaneseCalendar && identifier != kCFIslamicCalendar && identifier != kCFIslamicCivilCalendar && identifier != kCFHebrewCalendar && identifier != kCFChineseCalendar) {
if (CFEqual(kCFGregorianCalendar, identifier)) identifier = kCFGregorianCalendar;
else if (CFEqual(kCFBuddhistCalendar, identifier)) identifier = kCFBuddhistCalendar;
else if (CFEqual(kCFJapaneseCalendar, identifier)) identifier = kCFJapaneseCalendar;
else if (CFEqual(kCFIslamicCalendar, identifier)) identifier = kCFIslamicCalendar;
else if (CFEqual(kCFIslamicCivilCalendar, identifier)) identifier = kCFIslamicCivilCalendar;
else if (CFEqual(kCFHebrewCalendar, identifier)) identifier = kCFHebrewCalendar;
// else if (CFEqual(kCFChineseCalendar, identifier)) identifier = kCFChineseCalendar;
else return NULL;
}
struct __CFCalendar *calendar = NULL;
uint32_t size = sizeof(struct __CFCalendar) - sizeof(CFRuntimeBase);
calendar = (struct __CFCalendar *)_CFRuntimeCreateInstance(allocator, CFCalendarGetTypeID(), size, NULL);
if (NULL == calendar) {
return NULL;
}
calendar->_identifier = (CFStringRef)CFRetain(identifier);
calendar->_locale = NULL;
calendar->_localeID = CFLocaleGetIdentifier(CFLocaleGetSystem());
calendar->_tz = CFTimeZoneCopyDefault();
calendar->_cal = NULL;
return (CFCalendarRef)calendar;
}
_locale
用 NULL
初始化,_localeID
用系统区域设置的区域设置标识符初始化(在 iPhone 和模拟器上是一个空字符串)。 _cal
设置为 NULL
。
CFIndex CFCalendarGetFirstWeekday(CFCalendarRef calendar) {
CF_OBJC_FUNCDISPATCHV(CFCalendarGetTypeID(), CFIndex, calendar, firstWeekday);
__CFGenericValidateType(calendar, CFCalendarGetTypeID());
if (!calendar->_cal) __CFCalendarSetupCal(calendar);
if (calendar->_cal) {
return ucal_getAttribute(calendar->_cal, UCAL_FIRST_DAY_OF_WEEK);
}
return -1;
}
因此,因为 _cal
是 NULL
,所以调用了 __CFCalendarSetupCal
。
static void __CFCalendarSetupCal(CFCalendarRef calendar) {
calendar->_cal = __CFCalendarCreateUCalendar(calendar->_identifier, calendar->_localeID, calendar->_tz);
}
用空字符串 _localeID
调用 __CFCalendarCreateUCalendar
。
我可以在 iOS 11、12 和 13 上确认此行为。源代码是针对名为 CF-Lite 的东西,但我进一步反汇编了实际的CoreFoundation 框架和它做同样的事情...
call _CFLocaleGetSystem ; _CFLocaleGetSystem
mov rdi, rax ; argument "cf" for method _CFRetain
call _CFRetain ; _CFRetain
mov qword [r15+0x18], rax
call _CFTimeZoneCopyDefault ; _CFTimeZoneCopyDefault
mov qword [r15+0x20], rax
mov rbx, qword [r15+0x10]
mov rdi, qword [r15+0x18] ; argument "locale" for method _CFLocaleGetIdentifier
call _CFLocaleGetIdentifier ; _CFLocaleGetIdentifier
mov rdx, qword [r15+0x20] ; argument #3 for method ___CFCalendarCreateUCalendar
mov rdi, rbx ; argument #1 for method ___CFCalendarCreateUCalendar
mov rsi, rax ; argument #2 for method ___CFCalendarCreateUCalendar
call ___CFCalendarCreateUCalendar ; ___CFCalendarCreateUCalendar
... 使用来自 CFLocaleGetSystem
的 CFLocaleGetIdentifier
的空标识符。
当您查看 CFCalendarCreateWithIdentifier
文档时,没有关于当前语言环境、时区、......
更有趣的是这两种方法的区别(讨论部分):
+calendarWithIdentifier:
- 它包含有关当前语言环境的信息,...
-initWithCalendarIdentifier:
- 只字未提当前语言环境,...
但是没有区别,calendarWithIdentifier:
只是调用了alloc
& initWithCalendarIdentifier:
.
push rbp
mov rbp, rsp
push r14
push rbx
mov rbx, rdx
mov rsi, qword [0x3cb478] ; argument "selector" for method _objc_msgSend, @selector(alloc)
mov r14, qword [_objc_msgSend_390220] ; _objc_msgSend_390220
call r14 ; Jumps to 0x553ae0 (_objc_msgSend), _objc_msgSend
mov rsi, qword [0x3cc768] ; argument "selector" for method _objc_msgSend, @selector(initWithCalendarIdentifier:)
...
我认为这是一个文档问题,应该报告给 Apple(是吗,FB7740798)。