爲了最大限度地保證事件數據的準確性、完整性和及時性,數據採集 SDK 須要及時地將事件數據同步到服務端。但在某些狀況下,好比手機處於斷網環境,或者根據實際需求只能在 Wi-Fi 環境下才能同步數據等,可能會致使事件數據同步失敗或者沒法進行同步。所以,數據採集 SDK 須要先把事件數據緩存在本地,待符合必定的策略(條件)以後,再去同步數據[1]。ios
在 iOS 應用程序中,從 「數據緩存在哪裏」 這個維度看,緩存通常分爲兩種類型:git
內存緩存是將數據緩存在內存中,供應用程序直接讀取和使用。優勢是讀寫速度極快。缺點是因爲內存資源有限,應用程序在系統中申請的內存,會隨着應用程序生命週期的結束而被釋放。這就意味着,若是應用程序在運行的過程當中被用戶強殺或者出現崩潰的狀況,都有可能致使內存中緩存的數據丟失。所以,將事件數據緩存在內存中不是最佳選擇。github
磁盤緩存是將數據緩存在磁盤空間中,其特色正好與內存緩存相反。磁盤緩存容量大,可是讀寫速度相對於內存緩存來講要慢一些。不過磁盤緩存是持久化存儲,不受應用程序生命週期的影響。通常狀況下,一旦數據成功保存在磁盤中,丟失的風險就很是低。所以,即便磁盤緩存數據讀寫速度較慢,但綜合考慮下,磁盤緩存是緩存事件數據的最優選擇。sql
因爲磁盤緩存是一種能夠持久化存儲的方案,對於存儲事件數據是一種最優的選擇。在 iOS 中有多種持久化存儲的方案,好比 KeyChain、NSUserDefaults、文件存儲、數據庫存儲等均可以作持久化存儲。那咱們的事件數據使用哪一種方案比較好呢?數據庫
咱們知道 KeyChain、NSUserDefaults 是一種輕量級的存儲方案,好比登陸用戶的用戶名、登陸狀態等,使用 KeyChain 或者 NSUserDefaults 是一種不錯的選擇。可是對於大量的事件數據而言,這兩種存儲方案就無能爲力了。數組
文件存儲能夠知足存儲大量數據的需求,所以可使用文件來存儲採集的事件數據。其實,在 SDK 的一些前期版本,咱們就是使用文件來存儲事件數據的。文件存儲相對來講仍是比較簡單的,主要操做就是寫文件和讀文件。咱們每次都是將全部的數據寫入同一個文件,寫入的數據量越大,文件緩存性能越好。固然,文件存儲仍是不夠靈活的,咱們很難使用更細的粒度去操做數據,好比,很難對其中的某一條數據進行讀和寫的操做。緩存
有沒有其餘的方式,能夠知足對數據靈活操做的需求呢?答案是確定的,數據庫就知足這個需求。在 iOS 應用程序中,使用的數據庫通常是 SQLite 數據庫。SQLite 是一個輕量級的數據庫,數據存儲簡單高效,使用也很是簡單。相對於文件存儲來講,數據庫存儲更加靈活,能夠實現對單條數據的插入、查詢和刪除操做,同時調試也更容易[1]。async
實現 SDK 中的數據庫時,爲了保證數據的完整性和準確性,採用了較爲完善的存儲策略:工具
SDK 採集的事件數據中,會有不少字段,好比事件名稱、預置公共屬性和用戶自定義屬性等。雖然事件數據中包含的屬性比較多,可是存儲數據無需關心具體的細節,能夠將一個事件數據當作總體存儲到數據表的一個字段中,從而提升數據的操做效率。性能
具體的結構如表 3-1 所示:
表 3-1 事件數據的存儲結構
SDK 採集數據過程當中,會頻繁的執行緩存數據、上報數據和刪除數據等耗時操做。爲了保證 SDK 的數據採集不影響用戶的 App 性能,這些耗時的操做所有在子線程中完成。SDK 在執行數據存儲和數據上報會涉及到 SAEventStore 、SAEventFlush、SAHTTPSession、SAEventTracker 等幾個關鍵類:
3.3.1. 初始化工具類
_eventTracker = [[SAEventTracker alloc] initWithQueue:_serialQueue];
- (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; }
- (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; }
- (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. 數據入庫
- (BOOL)insertRecord:(SAEventRecord *)record { BOOL success = [self.database insertRecord:record]; if (!success) { [self.recordCaches addObject:record]; } return success; }
#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; } } }
- (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. 數據刪除
- (void)flushAllEventRecords { if (![self canFlush]) { return; } BOOL isFlushed = [self flushRecordsWithSize:self.isDebugMode ? 1 : 50]; if (isFlushed) { SALogInfo(@"Events flushed!"); } }
...... // 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); } }]; ......
當 SDK 調用 track 相關方法時,首先是 SDK 會對事件數據的各項屬性進行合法性校驗,校驗經過後將事件數據存儲到數據庫。在 SDK 初始化時啓動的定時器會定時檢查是否知足上報條件,當符合上報時,再將數據上報到服務端,最後再把上報成功的數據從數據庫中刪除。工做流程如圖 3-1 所示:
圖 3-1 數據採集流程
本文介紹了神策 iOS SDK[2] 中使用到的存儲方式和具體使用流程。但願經過這篇文章的介紹,你們可以對神策 iOS SDK 存儲模塊有一個較爲全面的瞭解。
參考文獻:
[1]王灼洲.iOS全埋點解決方案[M].北京:機械工業出版社,2020:162-197.