前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >UserDefaults 浅析及其使用管理

UserDefaults 浅析及其使用管理

作者头像
CoderStar
发布2022-08-24 15:04:54
1.1K0
发布2022-08-24 15:04:54
举报
文章被收录于专栏:CoderStar

前言

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-07-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CoderStar 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 对象实例
  • 相关问题
    • UserDefaults的存储范围
      • value(forKey:) 和 object(forKey:)
        • UserDefaults 底层也是使用的 plist 文件,那它和普通的 plist 文件读取有什么区别呢?
        • 使用管理
          • protocol
            • @propertyWrapper
            • 最后
              • 参考资料
              相关产品与服务
              对象存储
              对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档