UserDefaults 浅析及其使用管理 | 8月更文挑战

2,787 阅读9分钟

前言

Hi Coder,我是 CoderStar!

我想每一个 iOSer 对UserDefaults都有所了解,但大家真的完全了解它吗?下面,我谈谈我对UserDefaults的看法。

同时,这也应该是 iOS 持久化方式系列的开篇文章了。

对象实例

UserDefaults生成对象实例大概有以下三种方式:

open class var standard: UserDefaults { get }

public convenience init()

@available(iOS 7.0, *)
public init?(suiteName suitename: String?)

平时大家经常使用的应该是第一种方式,第二种方式和第一种方式产生的结果是一样的,实际上操作的都是 APP 沙箱中 Library/Preferences 目录下的以 bundle id 命名的 plist 文件,只不过第一种方式是获取到的是一个单例对象,而第二种方式每次获取到都是新的对象,从内存优化来看,很明显是第一种方式比较合适,其可以避免对象的生成和销毁。

如果一个 APP 使用了一些 SDK,这些 SDK 或多或少的会使用UserDefaults来存储信息,如果都使用前两种方式,这样就会带来一系列问题:

  • 各个 SDK 需要保证设置数据 KEY 的唯一性,以防止存取冲突;
  • plist 文件越来越大造成的读写效率问题;
  • 无法便捷的清除由某一个 SDK 创建的 UserDefaults 数据;

针对上述问题,我们可以使用第三种方式,也是本文主要介绍的一种方式。

@available(iOS 7.0, *)
public init?(suiteName suitename: String?)

根据传入的 suiteName的不同会产生四种情况:

  • 传入 nil:跟使用UserDefaults.standard效果相同;
  • 传入 bundle id:无效,返回 nil;
  • 传入 App Groups 配置中 Group ID:会操作 APP 的共享目录中创建的以Group ID命名的 plist 文件,方便宿主应用与扩展应用之间共享数据;
  • 传入其他值:操作的是沙箱中 Library/Preferences 目录下以 suiteName 命名的 `plist 文件。

相关问题

UserDefaults的存储范围

因为UserDefaults底层使用的plist文件,所以plist文件支持的数据类型就是UserDefaults的存储范围,其中包括ArrayDataDictionaryStringIntBoolFloatDoubleDate等基础数据类型。

对于不是基本数据类型的数据结构,需要自己通过JSONEncoderNSKeyedArchiver等方式将其转换为 Data,然后再将其存入UserDefaults中。

需要注意,UserDefaults的设计初衷就不是用来存储大数据的,因为为了提高取值时的效率,当应用启动时会自动加载 Userdefault 里所有的数据,如果数据量太大的话就会造成启动缓慢,影响性能。

因为UserDefaults存储的数据都是明文,没有经过加密,所以尽量不要使用UserDefaults存储敏感数据,即使使用,也要使用加密算法对其进行加密后再存储进去。

尺寸限制

UserDefaults中,有一个sizeLimitExceededNotification属性很清楚的回答了这个问题。

/**
NSUserDefaultsSizeLimitExceededNotification is posted on the main queue when more data is stored in user defaults than is allowed.
Currently there is no limit for local user defaults except on tvOS, where a warning notification will be posted at 512kB, and the process terminated at 1MB.
For ubiquitous defaults, the limit depends on the logged in iCloud user.
*/
@available(iOS 9.3, *)
public class let sizeLimitExceededNotification: NSNotification.Name

翻译过来就是

  • 除了 tvOS 之外,其他的系统是没有限制。
  • 在 tvOS 上,警告通知将在 512kB 处发布,进程在 1MB 处终止;

value(forKey:)object(forKey:)

首先明确这两者是完全不同的东西,value(forKey:)定义于NSKeyValueCoding,就是我们常说的 KVC,其并不是UserDefaults的直接方法,object(forKey:)才是。

但由于UserDefaults也是遵循了NSKeyValueCoding协议的,所以使用value(forKey:)也是可以获取到数据,但是不建议这种用法。在 UserDefaults 里面最好使用object(forKey:),这是标准用法。

UserDefaults 底层也是使用的 plist 文件,那它和普通的 plist 文件读取有什么区别呢?

主要区别是:UserDefaults会自动帮我们做 plist 文件的存取并在内存中做了缓存。其中需要注意的是UserDefaults对数据的操作影响plist文件的改变这一过程是异步的,也就是说你修改了UserDefaults某一个 key 的值,紧接着去获取这个 key 的值,得到的也会是修改后的值,但此时plist文件中对应的值可能还是修改前的。

从 iOS 8 开始,会有一个常驻进程 cfprefsd 来负责异步更新plist文件这一任务。所以 UserDefaultssynchronize函数废弃也是有道理的,因为其本质上保证不了调用之后会将值立即存储到 plist 文件中。看一下synchronize函数上的注释吧。

/**
-synchronize is deprecated and will be marked with the API_DEPRECATED macro in a future release.

-synchronize blocks the calling thread until all in-progress set operations have completed. This is no longer necessary. Replacements for previous uses of -synchronize depend on what the intent of calling synchronize was. If you synchronized...
- ...before reading in order to fetch updated values: remove the synchronize call
- ...after writing in order to notify another program to read: the other program can use KVO to observe the default without needing to notify
- ...before exiting in a non-app (command line tool, agent, or daemon) process: call CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication)
- ...for any other reason: remove the synchronize call
*/
open func synchronize() -> Bool

本质上,我们是可以通过文件操作的方式对 UserDefaults 的最终产物 plist 文件进行操作的,但这是有风险的,最好不要这么操作。

使用管理

经常会在一些项目中看到UserDefaults的数据存、取操作,key直接用的字符串魔法变量,搞到最后都不知道项目中UserDefaults到底用了哪些 key,对 key 的管理没有很好的重视起来。下面介绍两种UserDefaults使用管理的两种方式。

protocol

利用 Swift 中protocol可以有默认实现的特性,可以对UserDefaults进行有效的管理。

直接上代码吧,相信大家一看应该就能明白。

/// UserDefaults存储协议,建议用枚举去实现该协议
public protocol UserDefaultsProtocol {
    // MARK: - 存储key

    /// 存储key
    var key: String { get }

    // MARK: - 存在nil

    /// 获取值
    var object: Any? { get }

    /// 获取url
    var url: URL? { get }

    // MARK: - 存在nil,有默认值

    /// 获取字符串值
    var string: String? { get }
    /// 获取字符串值,默认值为空
    var stringValue: String { get }

    /// 获取字典值
    var dictionary: [String: Any]? { get }
    /// 获取字典值,默认值为空
    var dictionaryValue: [String: Any] { get }

    /// 获取列表值
    var array: [Any]? { get }
    /// 获取列表值,默认值为空
    var arrayValue: [Any] { get }

    /// 获取字符串列表值
    var stringArray: [String]? { get }
    /// 获取字符串列表值,默认值为空
    var stringArrayValue: [String] { get }

    /// 获取Data值
    var data: Data? { get }
    /// 获取Data值,默认值为空
    var dataValue: Data { get }

    // MARK: - 不存在nil

    /// 获取Bool值,有默认值
    var bool: Bool { get }

    /// 获取int值,有默认值
    var int: Int { get }

    /// 获取float值,有默认值
    var float: Float { get }

    /// 获取double值,有默认值
    var double: Double { get }

    // MARK: - 方法

    /// 存储
    /// - Parameter object: 存储object型
    func save(object: Any?)

    /// 存储
    /// - Parameter int: 存储int型
    func save(int: Int)

    /// 存储
    /// - Parameter float: 存储float型
    func save(float: Float)

    /// 存储
    /// - Parameter double: 存储double型
    func save(double: Double)

    /// 存储
    /// - Parameter bool: 存储bool型
    func save(bool: Bool)

    /// 存储
    /// - Parameter url: 存储url型
    func save(url: URL?)

    /// 移除
    func remove()
}

// MARK: - 协议方法及计算属性实现

extension UserDefaultsProtocol {
    // MARK: - 存在nil

    /// 获取object
    public var object: Any? {
        return UserDefaults.standard.object(forKey: key)
    }

    /// 获取url
    public var url: URL? {
        return UserDefaults.standard.url(forKey: key)
    }

    // MARK: - 存在nil,有默认值

    /// 获取字符串值
    public var string: String? {
        return UserDefaults.standard.string(forKey: key)
    }

    /// 获取字符串值,默认值为空
    public var stringValue: String {
        return UserDefaults.standard.string(forKey: key) ?? ""
    }

    /// 获取字典值
    public var dictionary: [String: Any]? {
        return UserDefaults.standard.dictionary(forKey: key)
    }

    /// 获取字典值,默认值为空
    public var dictionaryValue: [String: Any] {
        return UserDefaults.standard.dictionary(forKey: key) ?? [String: Any]()
    }

    /// 获取列表值
    public var array: [Any]? {
        return UserDefaults.standard.array(forKey: key)
    }

    /// 获取列表值,默认值为空
    public var arrayValue: [Any] {
        return UserDefaults.standard.array(forKey: key) ?? [Any]()
    }

    /// 获取字符串列表值
    public var stringArray: [String]? {
        return UserDefaults.standard.stringArray(forKey: key)
    }

    /// 获取字符串列表值,默认值为空
    public var stringArrayValue: [String] {
        return UserDefaults.standard.stringArray(forKey: key) ?? [String]()
    }

    /// 获取Data值
    public var data: Data? {
        return UserDefaults.standard.data(forKey: key)
    }

    /// 获取Data值,默认值为空
    public var dataValue: Data {
        return UserDefaults.standard.data(forKey: key) ?? Data()
    }

    // MARK: - 不存在nil

    /// 获取Bool值
    public var bool: Bool {
        return UserDefaults.standard.bool(forKey: key)
    }

    /// 获取int值
    public var int: Int {
        return UserDefaults.standard.integer(forKey: key)
    }

    /// 获取float值
    public var float: Float {
        return UserDefaults.standard.float(forKey: key)
    }

    /// 获取double值
    public var double: Double {
        return UserDefaults.standard.double(forKey: key)
    }

    // MARK: - 方法

    /// 存储
    /// - Parameter value: 存储object
    public func save(object: Any?) {
        UserDefaults.standard.set(object, forKey: key)
    }

    /// 存储
    /// - Parameter int: 存储int型
    public func save(int: Int) {
        UserDefaults.standard.set(int, forKey: key)
    }

    /// 存储
    /// - Parameter float: 存储float型
    public func save(float: Float) {
        UserDefaults.standard.set(float, forKey: key)
    }

    /// 存储
    /// - Parameter double: 存储double型
    public func save(double: Double) {
        UserDefaults.standard.set(double, forKey: key)
    }

    /// 存储
    /// - Parameter bool: 存储bool型
    public func save(bool: Bool) {
        UserDefaults.standard.set(bool, forKey: key)
    }

    /// 存储
    /// - Parameter url: 存储url型
    public func save(url: URL?) {
        UserDefaults.standard.set(url, forKey: key)
    }

    /// 移除
    public func remove() {
        UserDefaults.standard.removeObject(forKey: key)
    }
}

上述协议主要是将UserDefaults的数据存取操作在协议中定义出来,并给出了协议默认方法实现。在取值的方法上借鉴了SwiftyJSON的思想,为每种基本结构提供可选值及非可选值两种方式,在使用时可根据自己的使用场景灵活使用。

我们如何进行使用呢?见下方代码示例,相关说明见注释。

/// 定义枚举,统一管理 UserDefaults的所有key
enum UserInfoEnum: String {
    case name
    case age
}

extension UserInfoEnum: UserDefaultsProtocol {
    /// 存储key值,可增加前缀、后缀等
    var key: String {
        return "CoderStar_\(rawValue)" rawValue
    }

    /// UserDefaults示例,协议默认实现为 UserDefaults.standard
    /// 如果想存储在另外的plist文件中,便可以单独实现
    var userDefaults: UserDefaults {
        return UserDefaults(suiteName: "CoderStar") ?? UserDefaults.standard
    }
}


func test() {
   /// 存
   UserInfoEnum.age.save(int: 18)

   /// 取
   let name = UserInfoEnum.age.int
}

如果公众号看代码不方便,可以直接访问UserDefaultsProtocol.swift进行查看。

@propertyWrapper

Swift 5.1 推出了为 SwiftUI 量身定做的@propertyWrapper关键字,翻译过来就是属性包装器,有点类似 java 中的元注解,它的推出其实可以简化很多属性的存储操作,使用场景比较丰富,用来管理UserDefaults只是其使用场景中的一种而已。

先上代码,相关说明请看代码注释。

@propertyWrapper
public struct UserDefaultWrapper<T> {
    let key: String
    let defaultValue: T
    let userDefaults: UserDefaults

    /// 构造函数
    /// - Parameters:
    ///   - key: 存储key值
    ///   - defaultValue: 当存储值不存在时返回的默认值
    public init(_ key: String, defaultValue: T, userDefaults: UserDefaults = UserDefaults.standard) {
        self.key = key
        self.defaultValue = defaultValue
        self.userDefaults = userDefaults
    }

    /// wrappedValue是@propertyWrapper必须需要实现的属性
    /// 当操作我们要包裹的属性时,其具体的set、get方法实际上走的都是wrappedValue的get、set方法
    public var wrappedValue: T {
        get {
            return userDefaults.object(forKey: key) as? T ?? defaultValue
        }
        set {
            userDefaults.setValue(newValue, forKey: key)
        }
    }
}

// MARK: - 使用示例

enum UserDefaultsConfig {
    /// 是否显示指引
    @UserDefaultWrapper("hadShownGuideView", defaultValue: false)
    static var hadShownGuideView: Bool

    /// 用户名称
    @UserDefaultWrapper("username", defaultValue: "")
    static var username: String

    /// 保存用户年龄
    @UserDefaultWrapper("age", defaultValue: nil)
    static var age: Int?
}

func test() {
  /// 存
  UserDefaultsConfig.hadShownGuideView = true
  /// 取
  let hadShownGuideView = UserDefaultsConfig.hadShownGuideView
}

最后

一定要更加努力呀!

Let's be CoderStar!


有一个技术的圈子与一群同道众人非常重要,来我的技术公众号,这里只聊技术干货。

微信公众号:CoderStar