UserDefaults 浅析及其使用管理

2022-08-24 15:04:54 浏览数 (1)

前言

Hi Coder,我是 CoderStar!

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

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

对象实例

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

代码语言:javascript复制
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 数据;

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

代码语言:javascript复制
@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的存储范围,其中包括ArrayDataDictionaryStringIntBoolFloatDouble等基础数据类型。

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

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

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

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函数上的注释吧。

代码语言:javascript复制
/**
-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进行有效的管理。

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

代码语言:javascript复制
/// 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]( "String: Any")
    }

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

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

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

    /// 获取字符串列表值,默认值为空
    public var stringArrayValue: [String] {
        return UserDefaults.standard.stringArray(forKey: key) ?? [String]( "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的思想,为每种基本结构提供可选值及非可选值两种方式,在使用时可根据自己的使用场景灵活使用。

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

代码语言:javascript复制
/// 定义枚举,统一管理 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[1]进行查看,或者点击查看原文进行查看。

@propertyWrapper

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

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

代码语言:javascript复制
@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!

参考资料

[1]UserDefaultsProtocol.swift: https://github.com/Coder-Star/LTXiOSUtils/blob/master/LTXiOSUtils/Classes/Util/UserDefaultsProtocol.swift

0 人点赞