SDK 是如何存儲事件數據的?

1、前言

爲了最大限度地保證事件數據的準確性、完整性和及時性,數據採集 SDK 須要及時地將事件數據同步到服務端。但在某些狀況下,好比手機處於斷網環境,或者根據實際需求只能在 Wi-Fi 環境下才能同步數據等,可能會致使事件數據同步失敗或者沒法進行同步。所以,數據採集 SDK 須要先把事件數據緩存在本地,待符合必定的策略(條件)以後,再去同步數據[1]。ios

2、數據存儲方式

在 iOS 應用程序中,從 「數據緩存在哪裏」 這個維度看,緩存通常分爲兩種類型:git

  • 內存緩存
  • 磁盤緩存

內存緩存是將數據緩存在內存中,供應用程序直接讀取和使用。優勢是讀寫速度極快。缺點是因爲內存資源有限,應用程序在系統中申請的內存,會隨着應用程序生命週期的結束而被釋放。這就意味着,若是應用程序在運行的過程當中被用戶強殺或者出現崩潰的狀況,都有可能致使內存中緩存的數據丟失。所以,將事件數據緩存在內存中不是最佳選擇。github

磁盤緩存是將數據緩存在磁盤空間中,其特色正好與內存緩存相反。磁盤緩存容量大,可是讀寫速度相對於內存緩存來講要慢一些。不過磁盤緩存是持久化存儲,不受應用程序生命週期的影響。通常狀況下,一旦數據成功保存在磁盤中,丟失的風險就很是低。所以,即便磁盤緩存數據讀寫速度較慢,但綜合考慮下,磁盤緩存是緩存事件數據的最優選擇。sql

因爲磁盤緩存是一種能夠持久化存儲的方案,對於存儲事件數據是一種最優的選擇。在 iOS 中有多種持久化存儲的方案,好比 KeyChain、NSUserDefaults、文件存儲、數據庫存儲等均可以作持久化存儲。那咱們的事件數據使用哪一種方案比較好呢?數據庫

咱們知道 KeyChain、NSUserDefaults 是一種輕量級的存儲方案,好比登陸用戶的用戶名、登陸狀態等,使用 KeyChain 或者 NSUserDefaults 是一種不錯的選擇。可是對於大量的事件數據而言,這兩種存儲方案就無能爲力了。數組

文件存儲能夠知足存儲大量數據的需求,所以可使用文件來存儲採集的事件數據。其實,在 SDK 的一些前期版本,咱們就是使用文件來存儲事件數據的。文件存儲相對來講仍是比較簡單的,主要操做就是寫文件和讀文件。咱們每次都是將全部的數據寫入同一個文件,寫入的數據量越大,文件緩存性能越好。固然,文件存儲仍是不夠靈活的,咱們很難使用更細的粒度去操做數據,好比,很難對其中的某一條數據進行讀和寫的操做。緩存

有沒有其餘的方式,能夠知足對數據靈活操做的需求呢?答案是確定的,數據庫就知足這個需求。在 iOS 應用程序中,使用的數據庫通常是 SQLite 數據庫。SQLite 是一個輕量級的數據庫,數據存儲簡單高效,使用也很是簡單。相對於文件存儲來講,數據庫存儲更加靈活,能夠實現對單條數據的插入、查詢和刪除操做,同時調試也更容易[1]。async

3、事件數據存儲

3.1 存儲策略

實現 SDK 中的數據庫時,爲了保證數據的完整性和準確性,採用了較爲完善的存儲策略:工具

  1. 開發者在初始化 SDK 時,能夠根據須要經過 - setMaxCacheSize: 方法設置本地緩存事件的最大條數。本地緩存事件的默認值是 10000 條。當開發者設置的最大緩存事件條數小於 10000 時,則使用默認值;
  2. 執行數據採集任務時,採集的數據首先緩存到本地數據庫。數據寫入時,會判斷數據庫裏緩存的事件條數是否超過設定的最大值;若是超過設定的最大緩存事件條數,則刪除最早入庫的 100 條數據,而後執行入庫操做;
  3. SDK 會定時檢查是否知足上報策略,知足上報策略時,會把數據庫裏的數據打包上報到服務端,上報成功後會刪除已上報的數據,上報失敗則不刪除。

3.2 數據庫表的設計

SDK 採集的事件數據中,會有不少字段,好比事件名稱、預置公共屬性和用戶自定義屬性等。雖然事件數據中包含的屬性比較多,可是存儲數據無需關心具體的細節,能夠將一個事件數據當作總體存儲到數據表的一個字段中,從而提升數據的操做效率。性能

具體的結構如表 3-1 所示:
image
表 3-1 事件數據的存儲結構

3.3 具體實現

SDK 採集數據過程當中,會頻繁的執行緩存數據、上報數據和刪除數據等耗時操做。爲了保證 SDK 的數據採集不影響用戶的 App 性能,這些耗時的操做所有在子線程中完成。SDK 在執行數據存儲和數據上報會涉及到 SAEventStore 、SAEventFlush、SAHTTPSession、SAEventTracker 等幾個關鍵類:

  • SAEventStore: 負責事件數據的存儲操做;
  • SAEventFlush: 負責數據的上報;
  • SAHTTPSession: 負責將上報數據的任務添加到隊列,等待執行;
  • SAEventTracker: 負責 track 事件和檢查是否達到上報條件。

3.3.1. 初始化工具類

  1. 在初始化 SDK 時,會對 SAEventTracker 工具類進行初始化:
_eventTracker = [[SAEventTracker alloc] initWithQueue:_serialQueue];
  1. 在 SAEventTracker 的初始化方法裏對 SAEventStore 和 SAEventFlush 兩個工具類進行初始化:
- (instancetype)initWithQueue:(dispatch_queue_t)queue {
    self = [super init];
    if (self) {
        _queue = queue;
 
        dispatch_async(self.queue, ^{
            self.eventStore = [[SAEventStore alloc] initWithFilePath:[SAFileStore filePath:@"message-v2"]];
            self.eventFlush = [[SAEventFlush alloc] init];
        });
    }
    return self;
}
  1. 初始化 SAEventStore 時,傳入的 filePath 參數是用於建立數據庫的路徑。SAEventStore 的初始化以下:
- (instancetype)initWithFilePath:(NSString *)filePath {
    self = [super init];
    if (self) {
        NSString *label = [NSString stringWithFormat:@"cn.sensorsdata.SAEventStore.%p", self];
        _serialQueue = dispatch_queue_create(label.UTF8String, DISPATCH_QUEUE_SERIAL);
        // 直接初始化,防止數據庫文件,意外刪除等問題
        _recordCaches = [NSMutableArray array];
 
        [self setupDatabase:filePath];
    }
    return self;
}
  1. 在方法 - setupDatabase: 裏對封裝了數據庫的工具類 SADatabase 初始化,在 SADatabase 建立了數據庫文件和表:
- (instancetype)initWithFilePath:(NSString *)filePath {
    self = [super init];
    if (self) {
        _filePath = filePath;
        _serialQueue = dispatch_queue_create("cn.sensorsdata.SADatabaseSerialQueue", DISPATCH_QUEUE_SERIAL);
        [self createStmtCache];
        [self open];
        [self createTable];
    }
    return self;
}

3.3.2. 數據入庫

  1. 對於校驗成功的數據,會嘗試把數據存入到數據庫,若是數據庫打開失敗,會把數據先保存在內存中的一個數組中:
- (BOOL)insertRecord:(SAEventRecord *)record {
    BOOL success = [self.database insertRecord:record];
    if (!success) {
        [self.recordCaches addObject:record];
    }
    return success;
}
  1. 在監聽到數據庫建立成功時,會嘗試把緩存在內存中的數據插入數據庫,若是插入失敗,會重試 3 次:
#pragma mark - observe
 
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context != SAEventStoreContext) {
        return [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    if (![keyPath isEqualToString:SAEventStoreObserverKeyPath]) {
        return;
    }
    if (![change[NSKeyValueChangeNewKey] boolValue] || self.recordCaches.count == 0) {
        return;
    }
    // 對於內存中的數據,重試 3 次插入數據庫中。
    for (NSInteger i = 0; i < 3; i++) {
        if ([self.database insertRecords:self.recordCaches]) {
            [self.recordCaches removeAllObjects];
            return;
        }
    }
}
  1. 插入事件數據是比較頻繁的操做,若是每次都作 「預解析 SQL 語句」 的操做,將會形成資源的大量浪費。對於插入數據來講,每次操做的 SQL 語句都是相同的,所以 「預解析 SQL 語句」 只需執行一次便可。因爲每次須要綁定不一樣的數據,咱們只須要重置一下以前的 sqlite3_stmt,而後從新綁定新的數據便可[1]。插入數據的邏輯以下:
- (BOOL)insertRecord:(SAEventRecord *)record {
    if (![record isValid]) {
        SALogError(@"%@ input parameter is invalid for addObjectToDatabase", self);
        return NO;
    }
    if (![self databaseCheck]) {
        return NO;
    }
 
    if (![self preCheckForInsertRecords:1]) {
        return NO;
    }
 
    NSString *query = @"INSERT INTO dataCache(type, content) values(?, ?)";
    sqlite3_stmt *insertStatement = [self dbCacheStmt:query];
    int rc;
    if (insertStatement) {
        sqlite3_bind_text(insertStatement, 1, [record.type UTF8String], -1, SQLITE_TRANSIENT);
        sqlite3_bind_text(insertStatement, 2, [record.content UTF8String], -1, SQLITE_TRANSIENT);
        rc = sqlite3_step(insertStatement);
        if (rc != SQLITE_DONE) {
            SALogError(@"insert into dataCache table of sqlite fail, rc is %d", rc);
            return NO;
        }
        self.count++;
        SALogDebug(@"insert into dataCache table of sqlite success, current count is %lu", self.count);
        return YES;
    } else {
        SALogError(@"insert into dataCache table of sqlite error");
        return NO;
    }
}

3.3.3. 數據刪除

  1. 在達到上報條件時,會觸發數據上報。默認狀況下是每 15 秒上報一次,或者緩存的數據達到 100 條時進行一次上報。在非 Debug 模式下,每次上報 50 條數據:
- (void)flushAllEventRecords {
    if (![self canFlush]) {
        return;
    }
    BOOL isFlushed = [self flushRecordsWithSize:self.isDebugMode ? 1 : 50];
    if (isFlushed) {
        SALogInfo(@"Events flushed!");
    }
}
  1. 對於已經上報成功的數據,SDK 會將其從數據庫中移除,防止數據的重複上報:
......
// flush
__weak typeof(self) weakSelf = self;
[self.eventFlush flushEventRecords:encryptRecords completion:^(BOOL success) {
__strong typeof(weakSelf) strongSelf = weakSelf;
void(^block)(void) = ^ {
if (!success) {
[strongSelf.eventStore updateRecords:recordIDs status:SAEventRecordStatusNone];
return;
}
// 5. 刪除數據
if ([strongSelf.eventStore deleteRecords:recordIDs]) {
[strongSelf flushRecordsWithSize:size];
}
};
if (sensorsdata_is_same_queue(strongSelf.queue)) {
block();
} else {
dispatch_sync(strongSelf.queue, block);
}
}];
......

3.4 數據流程

當 SDK 調用 track 相關方法時,首先是 SDK 會對事件數據的各項屬性進行合法性校驗,校驗經過後將事件數據存儲到數據庫。在 SDK 初始化時啓動的定時器會定時檢查是否知足上報條件,當符合上報時,再將數據上報到服務端,最後再把上報成功的數據從數據庫中刪除。工做流程如圖 3-1 所示:

圖 3-1 數據採集流程

4、總結

本文介紹了神策 iOS SDK[2] 中使用到的存儲方式和具體使用流程。但願經過這篇文章的介紹,你們可以對神策 iOS SDK 存儲模塊有一個較爲全面的瞭解。

參考文獻:

[1]王灼洲.iOS全埋點解決方案[M].北京:機械工業出版社,2020:162-197.

[2]https://github.com/sensorsdata/sa-sdk-ios

相關文章
相關標籤/搜索