概述
在App开发过程中,会遇到许多简略装备项的耐久化需求。比方App最近一次发动的时刻,App最后一次登陆的用户ID,用户首次运用功用的判别条件。而且跟着事务的扩展,零碎的装备还会不断增加。
UserDefaults
Apple供给了UserDefault结构来帮助咱们存储离散的装备,UserDefaults将以plist文件的方法存储在沙盒环境中。在不引入NoSql数据库的情况下,这是首推的计划。
注意事项
为了提高读取速度,App在发动时会将UserDefaults Standard对应的plist加载到内存中,如果文件过大就会增加App在发动时的加载时刻,一起提高一定的内存消耗。
所以在Standard中,咱们应该存放需求在App发动阶段当即获取的信息,比方用户最近登录的ID,App长途装备缓存的版本。
咱们能够通过火表来减缩Standard的数据量。运用UserDefaults的suiteName形式创立不同的装备表,这样装备项将存储到各自的plist文件中,这些独立的plist不会在发动时被主动加载。
装备办理的常见问题
-
运用硬编码的String Key将装备存储到UserDefaults中,通过复制粘贴Key的字符串来存取数据。
-
零星的运用UserDefaults,短少中心化办理计划。比方需求存储“开启告诉功用”的装备,Key通常会直接被放在事务相关代码中保护。
计划 1.0
办理UserDefaults
创立一个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
}
创立常量表
- 对装备项的Key进行中心化的注册与保护
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"
}
- 供给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数据源进行了封装,String Key的注册也一致到常量文件中。当咱们要查找或修改时,能够从装备表方便的查到String Key。
跟着事务的膨胀,装备项会越来越多,咱们会需求依据事务功用的分类,重新整理出多个分表。
随后咱们会发现一些问题:
-
String Key的注册尽管不麻烦,但Key中无法体现出Key归属与哪个UserDefaults。
-
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的运用情况,分析后向两个方向进行了优化:
- 供给中心化的装备方法,相关UserDefaults、保护String Key。
- 供给类ORM的办理方法,减少事务的接入本钱。
针对更杂乱的、类缓存调集的,或者有查询需求的装备项办理,请赶快用NoSQL替换,防止数据量上升带来的效率下降。