Lösung für die Persistenz der lokalen App-Konfiguration

Überblick

Während des App-Entwicklungsprozesses werden Sie auf viele Persistenzanforderungen für einfache Konfigurationselemente stoßen. Zum Beispiel der Zeitpunkt des letzten Starts der App, die Benutzer-ID der letzten Anmeldung bei der App und die Beurteilungsbedingungen für den Benutzer, die Funktion zum ersten Mal zu verwenden. Und mit der Expansion des Unternehmens wird die fragmentierte Konfiguration weiter zunehmen.

Benutzervorgaben

Apple stellt das UserDefault-Framework bereit, um uns beim Speichern diskreter Konfigurationen zu helfen, und UserDefaults werden in der Sandbox-Umgebung in Form von Plist-Dateien gespeichert. Dies ist die am meisten empfohlene Lösung ohne die Einführung einer NoSql-Datenbank.

Vorsichtsmaßnahmen

Um die Lesegeschwindigkeit zu verbessern, lädt die App beim Start die Plist entsprechend UserDefaults Standard in den Speicher.Wenn die Datei zu groß ist, erhöht dies die Ladezeit der App beim Start und erhöht den Speicherverbrauch auf ein gewisses Maß Ausmaß.

Daher sollten wir in Standard Informationen speichern, die sofort während der App-Startphase abgerufen werden müssen, z. B. die letzte Anmelde-ID des Benutzers und die Version des Remote-Konfigurationscache der App.

Wir können die Menge an Standarddaten reduzieren, indem wir Tabellen teilen. Verwenden Sie den suiteName-Modus von UserDefaults, um verschiedene Konfigurationstabellen zu erstellen, sodass die Konfigurationselemente in ihren eigenen Plist-Dateien gespeichert werden und diese unabhängigen Plists nicht automatisch beim Start geladen werden.

Häufig gestellte Fragen zum Konfigurationsmanagement

  1. Verwenden Sie den hartcodierten Zeichenfolgenschlüssel, um die Konfiguration in UserDefaults zu speichern, und greifen Sie auf die Daten zu, indem Sie die Zeichenfolge des Schlüssels kopieren und einfügen.

  2. Der verstreuten Verwendung von UserDefaults fehlt eine zentralisierte Verwaltungslösung. Wenn Sie beispielsweise die Konfiguration "Aktivieren der Benachrichtigungsfunktion" hinterlegen müssen, wird der Schlüssel normalerweise direkt im unternehmensbezogenen Code gepflegt.

Schema 1.0

Benutzervorgaben verwalten

Erstellen Sie eine UserDefault-Verwaltungsklasse. Der Hauptzweck besteht darin, das UserDefault-Framework zu schließen und die Nutzungsstrategie zu vereinheitlichen.

public class UserDefaultsManager {
    public static let shared = UserDefaultsManager()
    private init() {}
    public var suiteName:String? {
        didSet {
            /**
             根据传入的 suiteName的不同会产生四种情况:
             传入 nil:跟使用UserDefaults.standard效果相同;
             传入 bundle id:无效,返回 nil;
             传入 App Groups 配置中 Group ID:会操作 APP 的共享目录中创建的以Group ID命名的 plist 文件,方便宿主应用与扩展应用之间共享数据;
             传入其他值:操作的是沙箱中 Library/Preferences 目录下以 suiteName 命名的  plist 文件。
             */
            userDefault = UserDefaults(suiteName: suiteName) ?? UserDefaults.standard
        }
    }
    public var userDefault = UserDefaults.standard
}

复制代码

konstante Tabelle erstellen

  1. Zentralisierte Registrierung und Verwaltung des Schlüssels des Konfigurationselements
struct UserDefaultsKey {
    static let appLanguageCode              = "appLanguageCode"
    static let lastLaunchSaleDate           = "resetLastLaunchSaleDate"
    static let lastSaleDate                 = "lastSaleDate"
    static let lastSaveRateDate             = "lastSaveRateDate"
    static let lastVibrateTime              = "lastVibrateTime"
    static let exportedImageSaveCount       = "exportedImageSaveCount"
  
    static let onceFirstLaunchDate          = "onceFirstLaunchDate"
    static let onceServerUserIdStr          = "onceServerUserIdStr"
    static let onceDidClickCanvasButton     = "onceDidClickCanvasButton"
    static let onceDidClickCanvasTips       = "onceDidClickCanvasTips"
    static let onceDidClickEditBarGuide     = "onceDidClickEditBarGuide"
    static let onceDidClickEditFreestyleGuide   = "onceDidClickEditFreestyleGuide"
    static let onceDidClickManualCutoutGuide    = "onceDidClickManualCutoutGuide"
    static let onceDidClickBackgroundBlurGuide  = "onceDidClickBackgroundBlurGuide"
    static let onceDidTapCustomStickerBubble    = "onceDidTapCustomStickerBubble"
    static let onceDidRequestHomeTemplatesFromAPI = "onceDidRequestHomeTemplatesFromAPI"
  
  	static let firSaveExportTemplateKey     = "firSaveExportTemplateKey"
    static let firSaveTemplateDateKey       = "firSaveTemplateDateKey"
    static let firShareExportTemplateKey    = "firShareExportTemplateKey"
    static let firShareTemplateDateKey      = "firShareTemplateDateKey"
}
复制代码
  1. Stellen Sie die CURD-API bereit
private let appConfigUserDefaults = UserDefaultsManager(suiteName: "com.pe.config").userDefaults

var exportedImageSaveCount: Int { 
  return appConfigUserDefaults.integer(forKey: key) 
}

func increaseExportedImageSaveCount() {
		let key = UserDefaultsKey.exportedImageSaveCount
		var count = appConfigUserDefaults.integer(forKey: key)
		count += 1
		appConfigUserDefaults.setValue(count, forKey: key)
}
复制代码

Wir haben die UserDefaults-Datenquelle gekapselt, und die Registrierung des Zeichenfolgenschlüssels ist ebenfalls in der konstanten Datei vereinheitlicht. Wenn wir suchen oder ändern möchten, können wir den Zeichenfolgenschlüssel leicht aus der Konfigurationstabelle finden.

Wenn das Unternehmen expandiert, wird es immer mehr Konfigurationselemente geben, und wir müssen mehrere Untertabellen basierend auf der Klassifizierung von Geschäftsfunktionen neu anordnen.

Dann werden wir einige Probleme finden:

  1. String Key的注册虽然不麻烦,但Key中无法体现出Key归属与哪个UserDefaults。

  2. CURD API的数量会膨胀的更快,需要更多的维护成本。那么能不能将配置的管理更加面向对象,实现类似ORM的方式来管理呢?

方案2.0

根据上述的问题,来演化下方案2.0,我们来创建一个协议,用来规范UserDefaults的使用类。

它将包含CURD API的默认实现,初始化关联UserDefaults,自动生成String Key。

/// UserDefaults存储协议,建议用String类型的枚举去实现该协议
public protocol UserDefaultPreference {

    var userDefaults: UserDefaults { get }
    var key: String { get }

    var bool: Bool { get }
    var int: Int { get }
    var float: Float { get }
    var double: Double { get }

    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 }

    var data: Data? { get }
    var dataValue: Data { get }

    var object: Any? { get }
    var url: URL? { get }

    func codableObject<T: Decodable>(_ as:T.Type) -> T?

    func save<T: Encodable>(codableObject: T) -> Bool

    func save(string: String)
    func save(object: Any?)
    func save(int: Int)
    func save(float: Float)
    func save(double: Double)
    func save(bool: Bool)
    func save(url: URL?)
    func remove()
}
复制代码

定义完协议后,我们再添加一些默认实现,降低使用成本。

// 生成默认的String Key
public extension UserDefaultPreference where Self: RawRepresentable, Self.RawValue == String {
    var key: String {  return "\(type(of: self)).\(rawValue)" }
}

public extension UserDefaultPreference {
		// 默认使用 standard UserDefaults,可以在实现类中配置
    var userDefaults: UserDefaults { return UserDefaultsManager.shared.userDefaults }

    func codableObject<T: Decodable>(_ as:T.Type) -> T? {
        return UserDefaultsManager.codableObject(`as`, key: key, userDefaults: userDefaults)
    }

    @discardableResult
    func save<T: Encodable>(codableObject: T) -> Bool {
        return UserDefaultsManager.save(codableObject: codableObject, key: key, userDefaults: userDefaults)
    }

    var object: Any? { return userDefaults.object(forKey: key) }

    func hasKey() -> Bool { return userDefaults.object(forKey: key) != nil }

    var url: URL? { return userDefaults.url(forKey: key) }

    var string: String? { return userDefaults.string(forKey: key) }
    var stringValue: String { return string ?? "" }

    var dictionary: [String: Any]? { return userDefaults.dictionary(forKey: key) }
    var dictionaryValue: [String: Any] { return dictionary ?? [String: Any]() }

    var array: [Any]? { return userDefaults.array(forKey: key) }
    var arrayValue: [Any] { return array ?? [Any]() }

    var stringArray: [String]? { return userDefaults.stringArray(forKey: key) }
    var stringArrayValue: [String] { return stringArray ?? [String]() }

    var data: Data? { return userDefaults.data(forKey: key) }
    var dataValue: Data { return userDefaults.data(forKey: key) ?? Data() }

    var bool: Bool { return userDefaults.bool(forKey: key) }
    var boolValue: Bool? {
        guard hasKey() else { return nil }
        return bool
    }

    var int: Int { return userDefaults.integer(forKey: key) }
    var intValue: Int? {
        guard hasKey() else { return nil }
        return int
    }

    var float: Float { return userDefaults.float(forKey: key) }
    var floatValue: Float? {
        guard hasKey() else { return nil }
        return float
    }

    var double: Double { return userDefaults.double(forKey: key) }
    var doubleValue: Double? {
        guard hasKey() else { return nil }
        return double
    }

    func save(object: Any?) { userDefaults.set(object, forKey: key) }
    func save(string: String) { userDefaults.set(string, forKey: key) }
    func save(int: Int) { userDefaults.set(int, forKey: key) }
    func save(float: Float) { userDefaults.set(float, forKey: key) }
    func save(double: Double) { userDefaults.set(double, forKey: key) }
    func save(bool: Bool) { userDefaults.set(bool, forKey: key) }
    func save(url: URL?) { userDefaults.set(url, forKey: key) }

    func remove() { userDefaults.removeObject(forKey: key) }
}
复制代码

OK,我们来看下使用的案例

// MARK: - Launch
enum LaunchEventKey: String {
    case didShowLaunchGuideOnThisLaunch
    case launchGuideIsAlreadyShow
}
extension LaunchEventKey: UserDefaultPreference { }

func checkIfNeedLaunchGuide() -> Bool {
  return !LaunchEventKey.launchGuideIsAlreadyShow.bool  
}
func launchContentView() {
  LaunchEventKey.launchGuideIsAlreadyShow.save(bool: true)  
}

// MARK: - Language
enum LanguageEventKey: String {
    case appLanguageCode
}
extension LanguageEventKey: UserDefaultPreference { }

static var appLanguageCode: String {
  get {
    let code = LanguageEventKey.appLanguageCode.string ?? ""
    return code
  }
  set {
    LanguageEventKey.appLanguageCode.save(codableObject: newValue)
  }
}

// MARK: - Purchase
enum PurchaseStatusKey: String {
    case iapSubscribeExpireDate
}
extension PurchaseStatusKey: UserDefaultPreference { }

func handle() {
  let expirationDate: Date = Entitlement.expirationDate
  PurchaseStatusKey.iapSubscribeExpireDate.save(object: expirationDate)
}

func getValues() {
  let subscribeExpireDate = PurchaseStatusKey.iapSubscribeExpireDate.object as? Date
}

// MARK: - GlobalConfig
enum AppConfig: String {
    case globalConfig
}

private let appConfigUserDefaults = UserDefaultsManager(suiteName: "com.pe.AppConfig").userDefaults

extension AppConfig: UserDefaultPreference {
    var userDefaults: UserDefaults { return appConfigUserDefaults }
}

// 自定义类型
public class GlobalConfig: Codable {
    /// 配置版本号
    let configVersion: Int
    /// 用户初始试用次数
    let userInitialTrialCount: Int
    /// 生成时间 如:2022-09-19T02:58:31Z
    let createDate: String

    enum CodingKeys: String, CodingKey {
        case configVersion = "version"
        case userInitialTrialCount = "user_initial_trial_count"
        case createDate = "create_date"
    }
	  ...
}

lazy var globalConfig: GlobalConfig = {
  guard let config = AppConfig.globalConfig.codableObject(GlobalConfig.self) else {
    return GlobalConfig()
  }
  return config
}() {
  didSet { AppConfig.globalConfig.save(codableObject: globalConfig) }
}
复制代码

从上述案例可以看出,在配置项的注册和维护成本相对方案1.0有了大幅度的降低,对UserDefaults的使用进行了规范性的约束,提供了更方便的CURD API,使用方式也更加符合面向对象的习惯。

同时为了满足复杂结构体的存储需求,我们可以扩展实现Codable对象的存取逻辑。

总结

本方案的目的是解决乱象丛生的UserDefaults的使用情况,分析后向两个方向进行了优化:

  1. 提供中心化的配置方式,关联UserDefaults、维护String Key。
  2. 提供类ORM的管理方式,减少业务的接入成本。

针对更复杂的、类缓存集合的,或者有查询需求的配置项管理,请尽快用NoSQL替换,避免数据量上升带来的效率下降。

Guess you like

Origin juejin.im/post/7200945340122906681