再谈 NSUserDefaults

1,327 阅读3分钟
我们平时经常会用到 NSUserDefaults 这个东西,初学 Cocoa 的时候碰到它就感觉比 Windows 不知道友好到哪里去了,用它来保存用户偏好设置十分直观方便。

起初我认为不管是 User Defaults 还是 Property List 亦或是使用 NSCoder 序列化的对象,都是 plist 文件。因此认为 User Defaults 的机理实际上跟后两者没什么不同。的确,Apple 在广泛使用 plist 这种格式族来存储一些 Key-Value 数据,并且也有不同的表示方法,比如常见的类 xml 格式,还有使用比较多的二进制格式(Interface Builder 编译后的 nib 或 xib 文件也是它的变种)。然后这种格式由 CoreFoundation 来进行解析,以供应用层使用。

但事实上,NSUserDefaults 的原理与直接使用 plist 序列化有很大的区别。

首先我们要从 Domain 说起。我们知道,在 iOS 上,应用必须被沙盒化,各个不同的 apps 之间的 Defaults Domain 都不一样,通常是它们的 Bundle Identifier,或者是 App Group 中约定的所谓 Suite Name。然而除此以外,NSUserDefaults 还在使用许多 Domain 来维护其他的键值属性。当我们视图访问一个属性时,NSUserDefaults 会根据下面这个顺序,从不同的 Domain 中读取:

  1. NSArgumentDomain
  2. 应用的 Bundle Identifier
  3. NSGlobalDomain
  4. 系统语言的标识符
  5. NSRegistrationDomain

也就是说,不管什么应用,访问一个值都需要经历从上到下搜索各个 Domain 的过程,期间如果哪个 Domain 中有这个键,就会取出其对应的值。相反,如果最后一个 Domain 中仍然没有要找的键,那么就会返回一个 undefined result。

理解这些 Domain 十分重要。NSArgumentDomain 做 iOS 开发的话基本不会用到,它可以将命令行参数绑定到 NSUserDefaults 中,这样,如果一个键在命令行参数中存在了,即便用户也设置了自己的值,NSUserDefaults 也会乖乖地将命令行参数的值返回取出来。macOS 开发下,这是一个不错的调试方法。使用方式形如:

$ path/to/executable -key value

这样我们就可以在不真正改变属性值的同时,让应用表现出相应的行为。就像这样:

接下来是 NSGlobalDomain,顾名思义,这是所有应用都可以共享的一系列属性,我们其实无需关心这些属性,一般来说是系统 Frameworks 取来使用的,像上图的 AppleLanguages 就是一个存在于 NSGlobalDomain 的属性。


NSRegistrationDomain 是一个我们需要关心的 Domain,通过 registerDefaults: 这个方法可以将 Dictionary 中的一组属性附加到 NSRegistrationDomain 中,这是一个临时的 Domain,仅在进程中存在。我们可以利用这个 Domain 来规定一些偏好的默认值,因为当访问的键在上述所有 Domain 中都不存在时,就会 fallback 到 NSRegistrationDomain,从而读取出你之前注册好的默认值。

⚠️注意我标注斜体的 Domains,它们是 volatile 的 Domain,也就是说它们的数据不会被持久化到磁盘,随着下次启动应用,它们会丢失。当然,NSUserDefaults 允许你手动将它们清理掉,方法自己查一下 API 文档。

最后提一下同步的问题,NSUserDefaults 是一个线程安全的类,也就是说你可以在其它 queues 中任意使用 NSUserDefaults 而不用操心加锁的问题。用户设置的值会被缓存到内存中然后定时序列化到磁盘以提升性能。



没了。