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

前言

Hi Coder,我是 CoderStar!java

我想每个 iOSer 对UserDefaults都有所了解,但你们真的彻底了解它吗?下面,我谈谈我对UserDefaults的见解。git

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

对象实例

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 文件,只不过第一种方式是获取到的是一个单例对象,而第二种方式每次获取到都是新的对象,从内存优化来看,很明显是第一种方式比较合适,其能够避免对象的生成和销毁。swift

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

  • 各个 SDK 须要保证设置数据 KEY 的惟一性,以防止存取冲突;
  • plist 文件愈来愈大形成的读写效率问题;
  • 没法便捷的清除由某一个 SDK 建立的 UserDefaults 数据;

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

@available(iOS 7.0, *)
public init?(suiteName suitename: String?)
复制代码

根据传入的 suiteName的不一样会产生四种状况:markdown

  • 传入 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中。app

须要注意,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

相关文章
相关标签/搜索