为了最大限度地保证事件数据的准确性、完整性和及时性,数据采集 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.