iOS應用千萬級架構:存儲持久化


應用場景

iOS10后,發現在大量用戶在NSUserDefaults中取值失敗,導致一系列的持久化狀態丟失。故切換到SQLitie3持久化更適合大型項目的使用。

場景一:在大型項目中,經常需要我們寫的代碼支持可降級,對一些新功能進行灰度驗證。那運營開關是必不可少的。運營開關的配置就需要持久化設置了,即時網絡異常,也需要讀取上一次的正常記錄的。

場景二:在大型項目中,有一些資源是需要動態配置的,那就可以集中放在一個接口里處理。比如大促的動態圖、資源背景圖、彈窗提示文案等。當接口異常時,需要讀取本地數據庫里上次存儲的

場景三:用戶的常規操作設置,也需要持久化記錄,比如,新手指引彈框,當用戶點了不再提醒,就應該記錄到本地,下次不再提醒。還有就是超過多少次,不再提醒等一下用戶的設置

場景四:需要全局使用的數據,也可以放到內存中,但不存入數據庫,只當是內存的一個單例管理。

具體思路

  1. 依賴第三方庫FMDB,YYModel
  2. 基於FMDB,按業務建立不同的數據表,將需要持久化的數據,通過key-Value的存儲方式,存儲到sqlite3數據庫中
  3. 全局狀態量均由VSPersistence這個單例來進行管理,拆分為5個模塊: defaultPersistence / dynamicPersistence / memoryPersistence / switchPersistence / userPersistence
  4. APP啟動,將sqlite3的數據加載到JSPersistence中

場景及數據表說明

1.defaultPersistence(YSDefaultPersistence)

用於管理需要持久化的普通狀態量,可理解為不屬於這4個模塊(dynamicPersistence / memoryPersistence / switchPersistence / userPersistence)的狀態量。

2.dynamicPersistence(YSDynamicPersistence)

動態資源,APP動態配置的資源,APP啟動時,請求接口返回 

3.memoryPersistence(YSMemoryPersistence)

用於管理不需要持久化的配置項。

4.switchPersistence(YSSwitchPersistence)

運營開關,降級開關,APP啟動時請求接口返回

5.userPersistence(YSUserPersistence)

用於管理記錄用戶行為的配置項,如:記錄提示頁是否展示過,記錄提示頁上次展示的時間……

YSPersistence全局單例類

全局單例類,管理上面5個模塊場景

@interface YSPersistence : NSObject

/** 用於管理需要持久化的普通狀態量,可理解為不屬於這4個模塊(dynamicPersistence / memoryPersistence / switchPersistence / userPersistence)的狀態量 */
+ (YSDefaultPersistence *)defaultPersistence;

/** 動態資源,APP動態配置的資源,APP啟動時,請求接口返回 */
+ (YSDynamicPersistence *)dynamicPersistence;

/** 用於管理不需要持久化的配置項,僅存在內存中。比如某個接口數據返回,需要記錄某個字段的值 */
+ (YSMemoryPersistence *)memoryPersistence;

/** 運營開關,降級開關,APP啟動時請求接口返回 */
+ (YSSwitchPersistence *)switchPersistence;

/** 用於管理記錄用戶行為的配置項,如:記錄提示頁是否展示過,記錄提示頁上次展示的時間 */
+ (YSUserPersistence *)userPersistence;

@end

底層結構

1. YSKeyValueStack

輕量級的用於單例化FMDatabaseQueue的堆棧類,基於Key-Value的存儲方式,做了一些定制化的優化配置。

主要方法:

// 給數據表增加一個key-value
void YSKeyValueSetKeyValueToTable(FMDatabase *db, NSString *key, id value, NSString *tableName) {
    NSDictionary<NSString *, id> *JSONObject = @{ key: value };
    // [JSONObject yy_modelToJSONData]可能是因為多線程修改了內容導致崩潰
    NSData *data = [[JSONObject copy] yy_modelToJSONData];
    if (!data) {
#if DEBUG
        dispatch_async(dispatch_get_main_queue(), ^{
            @throw [NSException exceptionWithName:@"Key-Value 不是有效的JSON對象(上報到RTX吧)" reason:@"保持統一性" userInfo:nil];
        });
#endif
        return;
    }
    NSString *SQLScript = [NSString stringWithFormat:kSQLFormatterForReplaceValue, tableName, key];
    [db executeUpdate:SQLScript, data];
}

// 從數據表里刪除key-value
void JSKeyValueRemoveKeyValueToTable(FMDatabase *db, NSString *key, NSString *tableName) {
    NSString *SQLScript = [NSString stringWithFormat:kSQLFormatterForDeleteValue, tableName, key];
    [db executeUpdate:SQLScript];
}

2. NSObject+YSKeyValueModel

用於基於KeyValueStack的ORM,具體使用方法可參考代碼中的例子。

最主要的方法是,需要給每個屬性添加KVO監聽,當接口數據回來或者屬性的值有變化時,就會觸發KVO監聽方法,從而更新數據庫

/**
 * 監聽值的變化
 * 1、將需要變化的keyValue都存儲到VSKeyValueStackCachedChangeObjects中
 * 2、每隔1秒,將VSKeyValueStackCachedChangeObjects中的keyValue拷貝到局部變量cachedChangeObjects中,並清空VSKeyValueStackCachedChangeObjects里面所有的數據
 * 3、將cachedChangeObjects循環存儲到sqlite中
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id<VSKeyValueModel>)object change:(NSDictionary<NSString *, id> *)change context:(void *)context {
    if (context != VSKeyValueStackObserverContext) {
        return;
    }
    
    void(^batchHandler)(void) = ^(void) {
        UIBackgroundTaskIdentifier __block taskToken = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:NULL];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), VSKeyValueStackCachedChangeQueue, ^{
            NSArray<__VSKeyValueChange *> *cachedChangeObjects = [VSKeyValueStackCachedChangeObjects copy];
            [JSKeyValueStackCachedChangeObjects removeAllObjects];
            [self handleTransaction:^(FMDatabase *db) {
                for (__VSKeyValueChange *__change in cachedChangeObjects) {
                    @autoreleasepool {
                        // 如果值為空,則從數據表中移除;有值則更新
                        if (IsEmpty(__change.value)) {
                            VSKeyValueRemoveKeyValueToTable(db, __change.key, __change.tableName);
                        } else {
                            VSKeyValueSetKeyValueToTable(db, __change.key, __change.value, __change.tableName);
                        }
                    }
                }
                [[UIApplication sharedApplication] endBackgroundTask:taskToken];
                taskToken = UIBackgroundTaskInvalid;
            }];
        });
    };
    
    id value = change[NSKeyValueChangeNewKey];
    dispatch_async(JSKeyValueStackCachedChangeQueue, ^{
        // batchHandler 限定了每隔1秒,處理一次VSKeyValueStackCachedChangeObjects所有的key,並清空
        if (!VSKeyValueStackCachedChangeObjects.count) {
            batchHandler();
        }
        // 將需要變化的keyValue都存儲到VSKeyValueStackCachedChangeObjects中
        __JSKeyValueChange *__change = [__JSKeyValueChange new];
        __change.tableName = [object keyValueTableName];
        __change.key = keyPath;
        __change.value = value;
        [JSKeyValueStackCachedChangeObjects addObject:__change];
    });
}

使用方法

1. 新建一個YSDemoPersistence,實現協議YSKeyValueModel

2. 在YSDemoPersistence.h 中添加你需要存入數據庫的字段名,當然也可以存入數組和字典

3. 在YSDemoPersistence.m 中,給 -initWithKeyValueStack 初始化方法的所有屬性加上KVO監聽、dealloc 的時候移除監聽、實現協議keyValueTableName返回數據庫表名、如果有默認值,在

setUpWithDefaultValue設置。

@implementation YSUserPersistence

- (instancetype)initWithKeyValueStack
{
    self = [super initWithKeyValueStack];
    if (self) {
        [self addKeyPathObserverToKeyValueStack];
    }
    return self;
}

- (void)dealloc
{
    [self removeKeyPathObserverFromKeyValueStack];
}

#pragma mark - <YSKeyValueModel>

- (NSString *)keyValueTableName
{
    return @"UserPersistence";
}

// 設置屬性默認值
- (void)setUpWithDefaultValue
{
    _themeStyle = 0;
    _checkoutAgreeProtocolsMap = @{};
}

@end

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM