NSUserDefaults - 熟悉与陌生

swift 发布于 2017年09月23日
NSUserDefaults 常用场景

我们最常使用 NSUserDefaults 的场景大概就是这样,我们的 APP 需要保存一些配置信息,比如这样:

NSUserDefaults.standardUserDefaults().setBool(true, forKey: "isLogin")

设置完成后,这个配置就保存在 User Default System 的数据库中了,我们在以后需要的时候可以通过这样的方式来取得它的值:

NSUserDefaults.standardUserDefaults().boolForKey("isLogin")

相信对于 NSUserDefaults 的这种使用方式大家已经非常熟悉了。 但有一些细节还是值得我们仔细斟酌一番。 我们上面的代码使用 boolForKey 方法来取得 isLogin 的值。 这个调用看起来没有任何问题,除了一种情况之外。那就是,如果在获取 isLogin 的值之前它并没有被设置,应该返回什么呢? 如果使用 boolForKey,对于不存在的 key,返回的是 false。

这是一个默认值,但并不一定符合你的业务逻辑。比如你需要对 isLogin 真正为 false 的时候进行某些处理, 但 boolForKey 会对不存在的 key 也返回 false。 这种情况下,它就会扰乱你的业务逻辑。

那么更稳妥的办法是什么呢? 可以这样:

NSUserDefaults.standardUserDefaults().objectForKey("isLogin")

objectForKey 方法对于不存在的 key 值会返回 nil。 而不是像 boolForKey 那样返回一个默认的 false 值。 这样我们就可以通过 if let 来排除 key 尚未初始化的情况了:

if let isLogin = NSUserDefaults.standardUserDefaults().objectForKey("isLogin") as? Bool {

} else {

}

这样就解决了 boolForKey 方法的默认值问题。 其实 boolForKey 只是对 objectForKey 的一个封装,它的内部依然调用的是 objectForKey 方法。 同样的问题还存在于其他字段类型, 比如 integerForKey 方法。 默认情况下,如果所访问的 key 不存在的话 integerForKey 的返回值是 0, 这也会有可能会对我们真正的逻辑造成干扰。

registerDefaults

前面我们使用 objectForKey 方法避开了默认值的陷阱。 但这样也有它的缺陷,最明显的就是我们的代码量和逻辑变得复杂了。还有另外一种情况,我们虽然要分辨 key 所对应的值为空的情况,但只需要在它为空的时候指定一个我们自己的默认值即可。 这时候我们就可以使用 registerDefaults 方法了:

NSUserDefaults.standardUserDefaults().registerDefaults(["maxCount": 3])
NSUserDefaults.standardUserDefaults().integerForKey("maxCount") // 3

我们在使用 integerForKey 获取 maxCount 的值之前,先调用 registerDefaults 方法设置了它的默认值。 因为我们并没有用 setXXX 方法设置 maxCount, 所以这次取值应该是空的, 但由于在他之前又使用 registerDefaults 注册了默认值。 那么这时, 我们就会得到默认值 3 了。

怎么样,这个特性大家应该并不常见。还有一点要注意的,就是 registerDefaults 设置的默认值是不会持久化存储的,也就是说我们每次启动 APP 的时候,都需要这样设置一遍。

registerDefaults 原理

虽然从直观上看,registerDefaults 可以为我们的 key 设置默认值。 但它背后的机制还是值得咱们研究的。 我们将 key 传递给 NSUserDefaults 的 objectForKey 方法时,它会经历这样一个过程。 NSUserDefaults 还有一个 Domain 的概念,当我们调用 NSUserDefaults.standardUserDefaults() 方法时,就会初始化 NSUserDefaults, 并且它默认会包含 5 个 Domain, 分别是:

  • NSArgumentDomain
  • Application
  • NSGlobalDomain
  • Languages
  • NSRegistrationDomain

大家看到这里可能会有疑惑,这些又是什么鬼~, 简单来说, 我们调用的类似这样的方法:

NSUserDefaults.standardUserDefaults().setBool(true, forKey: "isLogin")

都是在 Application 这个域上面存储的,但 NSUserDefaults 还包括了其他 4 个域,那么为什么要有域这样的设计呢。这可以从 registerDefaults 方法说起。 registerDefaults 方法我们刚刚看到了,可以为指定的 key 注册默认值。但我们深入思考一下,这个默认值又是怎么存储和实现的呢?

其实 registerDefaults 所做的事情非常简单,只是将我们传递给它的参数都设置到了 NSRegistrationDomain 这个域中。 然后我们每次调用 NSUserDefaults.standardUserDefaults().integerForKey("maxCount") 这样的读取数据的方法时,实际上会在底层的存储结构中进行一次搜索,属性就是这样:

NSArgumentDomain -> Application -> NSGlobalDomain -> Languages -> NSRegistrationDomain

比如我们例子中的 maxCount, 由于我们之前使用 registerDefaults 将它设置到了 NSRegistrationDomain 域中,并且由于我们没有调用 setInteger 方法将它设置到 Application 域中。

所以按照 NSUserDefaults 的默认搜索顺序,就会找到最后 NSRegistrationDomain 域中的那个 maxCount, 也就是我们所谓的默认值 3 了。

设计思路

经过一番梳理,原来 NSUserDefaults 的内部是这么回事。那么为什么 NSUserDefaults 会用 Domain 这样的设计方式呢?主要原因是它不只是用作简单的信息存储,还有更多的设计用途。比如 NSArgumentDomain 这个域代表的是命令行参数,并且它的优先级最高。

我们可以在 Xcode 中为调试的应用设置命令行参数,进入任何一个项目,点击 Edit Scheme:

然后在 Scheme 管理界面中的 Arguments Passed On Launch 选项卡中添加命令行参数:

然后在应用启动的时候,我们可以使用 NSUserDefaults 来取得这个参数:

print(NSUserDefaults.standardUserDefaults().objectForKey("aString"))  // test

按照我们刚才讲的 NSUserDefaults 的搜索顺序,它会先搜索 NSArgumentDomain 这个域,因为所有的命令行参数都会存放的这个域中,所以我们能够在这个域中找到相应的值,就会输出 test。

怎么样,又了解到了 NSUserDefaults 的另一面了吧。

实际应用

刚才我们通过一个例子向大家展示了 NSUserDefaults 的另外一种使用场景。不过单纯的读取命令行参数的这种情况好像并不常用,咱们再来看一个更实用的例子。

多语言 APP 现在非常普遍,在 Xcode 中,我们创建一个 Localizable.strings 文件,然后将不同语言环境的文本分别存储到相应的文件中:

语言文件的内容就是这种形式,英文:

"tag" = "tags";

中文:

"tag" = "标记";

语言信息创建完成后, 我们就可以在 APP 中取到它:

print(NSLocalizedString("tag", comment: ""))

NSLocalizedString 函数会根据设备的当前语言配置取到对应的字符串,如果测试设备当前是英文环境,就会输出 tags, 如果是中文环境,就会输出 "标记" 两个字。

这个机制可以很方便让我们通过多个配置文件来将不同语言的文本信息分别存放和维护。这也是大多数开发平台的通用做法。不过还是有一些痛点。比如在我们调试 APP 的时候,我们往往需要把各个语言的版本都验证一遍。这时候我们为了看到不同语言的文本,就需要反复修改设备的语言配置信息,也挺麻烦的。

其实 NSUserDefaults 给我们提供了一个更加快捷的方式,还记得我们前面提到的 NSUSerDefaults 的 5 个默认域么? 其中一个域叫做 Languages。

是的,其实 NSLocalizedString 函数是通过 NSUserDefaults 的 Languages 域中的信息来确定当前设备语言环境的。也就是我们每次修改设备的语言设置,系统都会在 Languages 设置相应的字段, 这个字段的 key 就是 AppleLanguages

也就是说,当前设备的语言信息,其实是存放在 NSUserDefaults 中的。那么回想一下 NSUserDefaults 的搜索顺序,Languages 其实是在 NSArgumentDomain 的后面,那么是不是就意味着只要在命令行参数中指定了语言设置,就会覆盖原有的语言信息呢? 如果是这样,我们只需要用设置命令行参数的方式就可以调试多语言版本了。

我们在一个当前语言配置为英文的设备上,打开 APP 设置,然后设置 AppleLanguages 命令行参数为 (zh-Hans) :

然后运行程序,并在程序开始的时候调用这个语句:

print(NSLocalizedString("tag", comment: ""))  // 标记

你会发现,虽然当前设备的语言环境是英文,但 NSLocalizedString 识别的语言是中文。这就说明我们的命令行参数起作用了。 这个原理其实非常简单,回想一下我们之前列出的 NSUserDefaults 搜索顺序:

NSArgumentDomain -> Application -> NSGlobalDomain -> Languages -> NSRegistrationDomain

命令行参数输入 NSArgumentDomain 域,它的搜索优先级是最高的,所以语言设置也会以我们传进来的命令行参数为准。我们这里在命令行参数中设置 AppleLanguages 为 (zh-Hans),这个代表中文。这样即使设备本身的语言环境是英文,但我们应用中的语言环境也是中文了。

从这个例子上,大家不难想到 NSUserDefaults 要进行这样的分层设计的思路了。

结尾

我们深入探索了 NSUserDefaults 的更多特性,它除了能够保存我们应用中的一些简单配置信息之外,还有域的概念。并且还会有一个搜索顺序。熟练掌握这些域的机制,可能帮助我们实现很多的便捷操作。


如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。
关注微信公众号
发现更多精彩
swift-cafe