アプリのローカル構成永続化ソリューション

概要

アプリの開発プロセス中に、単純な構成アイテムに対して多くの永続化要件が発生します。例えば、アプリを最後に起動した時刻、アプリに最後にログインしたときのユーザーID、ユーザーが初めて機能を利用するための判断条件などです。そして、ビジネスが拡大するにつれて、断片化された構成は増加し続けます。

ユーザーデフォルト

Apple は、個別の構成を保存するのに役立つ UserDefault フレームワークを提供しており、UserDefaults は plist ファイルの形式でサンドボックス環境に保存されます。これは、NoSql データベースを導入しない最も推奨されるソリューションです。

予防

読み込み速度を向上させるため、アプリは起動時に UserDefaults Standard に対応した plist をメモリに読み込みます. ファイルが大きすぎると、起動時のアプリの読み込み時間が長くなり、メモリ消費量がある程度増加します.範囲。

そのため、Standard では、ユーザーの最新のログイン ID やアプリのリモート構成キャッシュのバージョンなど、アプリの起動段階ですぐに取得する必要がある情報を保存する必要があります。

テーブルを分割することで、標準データの量を減らすことができます。UserDefaults の suiteName モードを使用してさまざまな構成テーブルを作成し、構成アイテムが独自の plist ファイルに格納され、これらの独立した plist が起動時に自動的に読み込まれないようにします。

構成管理に関するよくある質問

  1. ハードコーディングされた文字列キーを使用して構成を UserDefaults に保存し、キーの文字列をコピーして貼り付けてデータにアクセスします。

  2. UserDefaults を分散して使用すると、集中管理ソリューションが不足します。たとえば、「通知機能を有効にする」という構成を格納する必要がある場合、通常、キーはビジネス関連のコードで直接保持されます。

スキーム 1.0

ユーザーのデフォルトの管理

UserDefault 管理クラスを作成します。主な目的は、UserDefault フレームワークを閉じて、使用戦略を統一することです。

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
}

复制代码

定数テーブルを作成する

  1. 構成アイテムのキーの登録と保守を一元化
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. CURD API を提供
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)
}
复制代码

UserDefaults データソースをカプセル化し、文字列キーの登録も定数ファイルに統一しました。検索または変更する場合は、構成テーブルから文字列キーを簡単に見つけることができます。

ビジネスが拡大するにつれて、構成項目が増え、ビジネス機能の分類に基づいて複数のサブテーブルを再配置する必要があります。

次に、いくつかの問題を見つけます。

  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替换,避免数据量上升带来的效率下降。

おすすめ

転載: juejin.im/post/7200945340122906681