如何提供真正的 iPhone 空语言环境

How to give a real iPhone null locale

我有一个 iOS 应用可以像这样获取国家/地区代码

NSLocale *currentLocale = [NSLocale autoupdatingCurrentLocale];
NSString *countryCode = [currentLocale objectForKey:NSLocaleCountryCode];

我知道从技术上讲这可以为空,但我假设(错误地)这只会发生在无关紧要的极端情况下。

但现在我有一个用户 phone 正在报告 null locale,它破坏了我的应用程序的一部分,因此他们无法使用它。在进行错误修复时,我只想了解如何进入和退出此状态,以便他们可以解决该错误。但即使他们将语言和区域设置为 English/USA,他们仍然会在语言和区域屏幕上得到这个奇怪的结果:

并且我的应用程序中的错误仍然由于 null locale/region 而出现。根据维基百科,这是底部的 0xa4“货币符号”:

The currency sign ¤ is a character used to denote an unspecified currency.

看来连iOS都不懂他们的语言环境。真正的 phone 是如何发生这种情况的?您如何解决这个问题?

他们在 iOS 14.7.1 上使用 iPhone 12 mini。

这是一个不幸且罕见的错误,似乎与用户首选项被破坏有关 on-device,并且在没有已知原因的情况下,除了尝试检测之外,您无能为力它并回退到一些默认语言环境(如 en_US)。

挖掘(来自 Hopper 的详细信息):

  • +[NSLocale autoupdatingCurrentLocale] 创建了一个 NSAutoLocale 的实例,一个私有 class 的实例,其初始值设定项获取 +[NSLocale currentLocale] 并监听 privately-defined 当前语言环境更改时的通知:

                         +[NSLocale autoupdatingCurrentLocale]:
    00000000000bdfd8         stp        x29, x30, [sp, #-0x10]!                     ; Objective C Implementation defined at 0x24fb80 (class method)
    00000000000bdfdc         mov        x29, sp
    00000000000bdfe0         adrp       x8, #0x353000                               ; &@selector(variantFormatter)
    00000000000bdfe4         ldr        x0, [x8, #0xf58]                            ; objc_cls_ref_NSAutoLocale,_OBJC_CLASS_$_NSAutoLocale
    00000000000bdfe8         bl         imp___stubs__objc_opt_new                   ; objc_opt_new
    00000000000bdfec         ldp        x29, x30, [sp], #0x10
    00000000000bdff0         b          imp___stubs__objc_autorelease               ; objc_autorelease
    
    int -[NSAutoLocale _init](int arg0) {
        r19 = arg0;
        r0 = [NSLocale currentLocale];
        r0 = [r0 retain];
        r8 = 0x35603c;
        asm { ldpsw      x8, x9, [x8] };
        *(r19 + r9) = r0;
        pthread_mutex_init(r19 + r8, 0x0);
        [[NSNotificationCenter defaultCenter] addObserver:r19 selector:@selector(_update:) name:@"kCFLocaleCurrentLocaleDidChangeNotification-4" object:0x0];
        r0 = r19;
        return r0;
    }
    

    NSAutoLocale 响应所有语言环境方法并将它们转发到底层 NSLocale 实例,因此我们可以查看 +currentLocale returns

  • +[NSLocale currentLocale] 只是 returns _CFLocaleCopyCurrent() 的结果(NSLocaleCFLocaleRef 是 toll-free 桥接):

    void +[NSLocale currentLocale]() {
        [_CFLocaleCopyCurrent() autorelease];
        return;
    }
    

    _CFLocaleCopyCurrent() returns 从多个地方使用不同参数调用的“Guts”函数的共享实现的内容:

    int _CFLocaleCopyCurrent() {
        rax = __CFLocaleCopyCurrentGuts(0x0, 0x1, 0x0, 0x0);
        return rax;
    }
    
  • __CFLocaleCopyCurrentGuts 太大而无法内联包含在此处,但是当使用给定参数调用时(并且当它没有缓存时“当前语言环境”实例),它通过从当前用户首选项

    中查找 AppleLocale 键的值来创建新的语言环境
    • 否则,它还会查找 AppleLanguages 字典并尝试根据其中一个值创建语言环境,但这是分开的

如此有效,“当前语言环境”只是一个语言环境 object,由 prefs 中的“AppleLocale”定义的语言环境标识符构成。查看的具体位置是 kCFPreferencesCurrentHostkCFPreferencesCurrentUserkCFPreferencesAnyApplication 应用程序(即全局首选项)——即 defaults read/write NSGlobalDomain ... 返回的内容(相当于 defaults read/write -g ...).

这个值你可以自己查看,因为我的机器在美国是设置成英文的,所以看到了

$ defaults read -g AppleLocale
en_US

您可以编写一个检查“AppleLocale”的小工具,向“AppleLocale”写入一个值,如果您愿意,可以重新运行它以检查更改,但这也是可行的 in-process (以一种令人费解的方式)。如果你从 Objective-C 某处 forward-declare _CFLocaleResetCurrent() (桥接 header 会起作用),你可以直接调用清除当前区域缓存的函数,并通过操作 CFPreferences 直接查看当前语言环境的变化:

import Foundation

func overwriteLocaleIdentifier(_ identifier: String) {
    // We use kCFPreferencesCurrentApplication to avoid changing system-wide settings.
    CFPreferencesSetAppValue("AppleLocale" as CFString, identifier as CFString, kCFPreferencesCurrentApplication)
    _CFLocaleResetCurrent()
}

func withOverwrittenLocaleIdentifier(_ identifier: String, _ action: () -> Void) {
    let currentIdentifier = Locale.current.identifier
    defer { overwriteLocaleIdentifier(currentIdentifier) }

    overwriteLocaleIdentifier(identifier)
    action()
}

print(Locale.current.identifier)
withOverwrittenLocaleIdentifier("he_IL") {
    print(Locale.current.identifier)
}

print(Locale.current.identifier)

对我来说,这会产生

en_US
he_IL
en_US

无论好坏,此行为意味着无论为“AppleLocale”键找到什么值,都将用作当前区域设置标识符。

如果“AppleLocale”值缺失,您将开始看到一些日志:

2021-09-15 16:47:52.013846-0400 LocaleExample[40427:9913027] [User Defaults] CFPrefsPlistSource<0x107b08420> (Domain: kCFPreferencesAnyApplication, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null), Contents Need Refresh: No): Value for key AppleLocale was (null). Expected en_US (defaults(40182): 2021-09-15 16:44:29 (EDT))

当区域设置标识符丢失(或设置为无意义)时,系统绝对会开始出现异常行为,因为区域设置标识符是直接使用的。


那么,这会在什么时候发生?在典型的 iOS 设备上,答案 应该 是“从不”,但事实并非如此。通过典型的UI,没有办法不小心将语言环境标识符设置为无效的东西,但我在野外看到用户的语言环境设置为"""en"(没有国家代码) ,或者只是丢失了,在手头的设备上,确认是库存,而不是越狱等。真正所需要的只是一个破坏“AppleLocale”值的行为不当过程,你可以结束在这种情况下。

除了回退到已知良好的本地(或像 en_US 这样的默认设置)之外,通常没有真正的资源,如果用户要求,请请求他们通过设置重置他们的区域设置(更改设置,然后改回来)。理想情况下,这将使它们恢复到已知的良好状态。