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

相關文章
相關標籤/搜索