前言

​ SDK 需要把事件数据缓冲到本地,待符合一定策略再去同步数据。

一、数据存储策略

​ 在 iOS 应用程序中,从 “数据缓冲在哪里” 这个纬度看,缓冲一般分两种类型。

  • 内存缓冲
  • 磁盘缓冲

​ 内存缓冲是将数据缓冲在内存中,供应用程序直接读取和使用。优点是读取速度快。缺点是由于内存资源有限,应用程序在系统中申请的内存,会随着应用生命周期结束而被释放,会导致内存中的数据丢失,因此将事件数据缓冲到内存中不是最佳选择。

​ 磁盘缓冲是将数据缓冲到磁盘空间中,其特点正好和磁盘缓冲相反。磁盘缓冲容量打,但是读写速度对于内存缓冲要慢点。不过磁盘缓冲可以持久化存储,不受应用程序生命周期影响。因为,将数据保存在磁盘中,丢失的风险比较低。即使磁盘缓冲数据速度较慢,但综合考虑,磁盘缓冲是缓冲事件数据最优的选择。

1.1 沙盒

​ iOS 系统为了保证系统的安全性,采用了沙盒机制(即每个应用程序都有自己的一个独立存储空间)。其原理就是通过重定向技术,把应用程序生成和修改的文件重定向到自身的文件中。因此,在 iOS 应用程序里,磁盘缓存的数据一般都存储在沙盒中。

​ 我们可以通过下面的方式获取沙盒路径:

  1. // 获取沙盒主目录路径
  2. NSString *homeDir = NSHomeDirectory();

​ 在模拟上,输出沙盒路径示例如下:

  1. /Users/renwei/Library/Developer/CoreSimulator/Devices/B1D7EC3E-BE72-4F8D-A4EF-E3D6316827CF/data/Containers/Data/Application/229B24A6-E13D-4DE6-9B52-363E832F9717

​ 沙盒的根目录下有三个常用的文件夹:

  • Document
  • Library
  • tmp

(1)Document 文件夹

​ 在 Document 文件夹中,保存的一般是应用程序本身产生的数据。

​ 获取 Document 文件夹路径的方法:

  1. NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask , YES).lastObject;
  1. /Users/renwei/Library/Developer/CoreSimulator/Devices/B1D7EC3E-BE72-4F8D-A4EF-E3D6316827CF/data/Containers/Data/Application/86212089-1D48-4B92-A919-AB87D3683191/Documents

(2) Library 文件夹

​ 获取 Library 文件夹路径方法:

  1. NSString *path = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask , NO).lastObject;
  1. /Users/renwei/Library/Developer/CoreSimulator/Devices/B1D7EC3E-BE72-4F8D-A4EF-E3D6316827CF/data/Containers/Data/Application/4BBA5D3E-0C75-4543-B831-AE3344DCC940/Library

在 Library 文件夹下有两个常用的子文件夹:

  • Caches
  • Preferences

​ Caches 文件夹主要用来保存应用程序运行时产生的需要持久化的数据,需要应用程序复制删除。

获取 Caches 文件夹路径的方法

  1. NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask , YES).lastObject;
  1. /Users/renwei/Library/Developer/CoreSimulator/Devices/B1D7EC3E-BE72-4F8D-A4EF-E3D6316827CF/data/Containers/Data/Application/38CEA9CA-4C49-4B94-84F3-16E434ABFE0F/Library/Caches

​ Preferences 文件保存的是应用程序的偏好设置,即 iOS 系统设置应用会从该目录中读取偏好设置信息,因此,该目录一般不用于存储应用程序产生的数据。

(3)tmp 文件夹

​ tmp 文件夹主要用于保存应用程序运行时参数的临时数据,使用后在将相应的文件从该目录中删除,不会对 tmp 文件中的数据进行备份。

​ 获取 tmp 文件路径的方法:

  1. NSString *path = NSTemporaryDirectory();
  1. /Users/renwei/Library/Developer/CoreSimulator/Devices/B1D7EC3E-BE72-4F8D-A4EF-E3D6316827CF/data/Containers/Data/Application/8E8906B8-0CBC-4A83-A220-A09F397304CD/tmp/

通过上面综合对比发现,最适合缓存事件数据的地方,就是 Library 下 Caches 文件夹中。

1.2 数据缓存

​ 在 iOS 应用程序中,一般通过两种方式进行磁盘缓存:

  • ​ 文件缓存
  • ​ 数据库缓存

​ 这两种方式都是可以实现数据采集 SDK 的缓冲机制。缓冲的策略即当事件发生后,先将事件数据存储在缓存中,待符合一定策略后从缓存中读取事件数据并进行同步,同步成功后,将已同步的事件从缓存中删除。

​ 对于写入的性能,SQLite 数据库优于文件缓存.

​ 对于读取的性能:如果单条数据小于 100KB,则 SQLite 数据库读取的速度更快。如果单条数据大于 100KB,则从文件中读取的速度更快。

​ 因此,数据采集 SDK 一般都是使用 SQLite 数据库来缓存数据,这样可以拥有最佳的读写性能。如果希望采集更完整,更全面的信息,比如采集用户操作时当前截图的信息(一般超过100KB),文件缓存可能是最优的选择。

二、文件缓存

​ 可以使用 NSKeyedArchiver 类将字典对象进行归档并写入文件,也可以使用 NSJSONSerialization 类把字典对象转成 JSON 格式字符串写入文件。

2.1 实现步骤

第一步:新建处理文件的工具类 SensorsAnalyticsFileStore ,在工具类中新增一个属性 filePath 用于保存存储文件的路径。在 SensorsAnalyticsFileStore 文件的 -init 方法中初始化 filePath 属性,我们默认在 Caches 目录下 SensorsAnalytics.plist 文件来缓存数据。

  1. @interface SensorsAnalyticsFileStore : NSObject
  2. /// 保存存储文件的路径
  3. @property (nonatomic, copy) NSString *filePath;
  4. @end
  1. static NSString * const SensorsAnalyticsDefaultFileName = @"SensorsAnalytics.plist";
  2. @implementation SensorsAnalyticsFileStore
  3. - (instancetype)init {
  4. self = [super init];
  5. if (self) {
  6. // 初始化默认的事件数据存储地址
  7. _filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultFileName];
  8. }
  9. return self;
  10. }
  11. @end

第二步:我们使用 NSJSONSerialization 类将字典对象转换成 JSON 格式并写入文件。新增 - saveEvent: 方法用于事件数据写入文件,同时,新增 NSMutableArray<NSDictionary *> *events;并在 - init 方法中进行初始化

  1. - (instancetype)init {
  2. self = [super init];
  3. if (self) {
  4. // 初始化默认的事件数据存储地址
  5. _filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultFileName];
  6. // 初始化事件数据,从文件路径中读取数据
  7. [self readAllEventsFromFilePath:_filePath];
  8. }
  9. return self;
  10. }
  11. - (void)saveEvent:(NSDictionary *)event {
  12. // 在数组中直接添加事件数据
  13. [self.events addObject:event];
  14. // 将事件数据保存在文件中
  15. [self writeEventsToFile];
  16. }
  17. - (void)writeEventsToFile {
  18. NSError *error = nil;
  19. // 将字典数据解析成 JSON 数据
  20. NSData *data = [NSJSONSerialization dataWithJSONObject:self.events options:NSJSONWritingPrettyPrinted error:&error];
  21. if (error) {
  22. return NSLog(@"The JSON object`s serialization error: %@", error);
  23. }
  24. // 将数据写入到文件
  25. [data writeToFile:self.filePath atomically:YES];
  26. }

第三步:在 SensorsAnalyticsSDK.m 文件中新增一个 SensorsAnalyticsFileStore 类型属性 fileStroe,并在 - init 方法中进行初始化

  1. #import "SensorsAnalyticsFileStore.h"
  2. /// 文件缓存事件数据对象
  3. @property (nonatomic, strong) SensorsAnalyticsFileStore *fileStroe;
  4. - (instancetype)init {
  5. self = [super init];
  6. if (self) {
  7. _automaticProperties = [self collectAutomaticProperties];
  8. // 设置是否需是被动启动标记
  9. _launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
  10. _loginId = [[NSUserDefaults standardUserDefaults] objectForKey:SensorsAnalyticsLoginId];
  11. _trackTimer = [NSMutableDictionary dictionary];
  12. _enterBackgroundTrackTimerEvents = [NSMutableArray array];
  13. _fileStroe = [[SensorsAnalyticsFileStore alloc] init];
  14. // 添加应用程序状态监听
  15. [self setupListeners];
  16. }
  17. return self;
  18. }

第四步:修改 SensorsAnalyticsSDK 的类别 Track 中的 - track: properties: 方法。

  1. - (void)track:(NSString *)eventName properties:(nullable NSDictionary<NSString *, id> *)properties {
  2. NSMutableDictionary *event = [NSMutableDictionary dictionary];
  3. // 设置事件 distinct_id 字段,用于唯一标识一个用户
  4. event[@"distinct_id"] = self.loginId ?: self.anonymousId;
  5. // 设置事件名称
  6. event[@"event"] = eventName;
  7. // 事件发生的时间戳,单位毫秒
  8. event[@"time"] = [NSNumber numberWithLong:NSDate.date.timeIntervalSince1970 *1000];
  9. NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
  10. // 添加预置属性
  11. [eventProperties addEntriesFromDictionary:self.automaticProperties];
  12. // 添加自定义属性
  13. [eventProperties addEntriesFromDictionary:properties];
  14. // 判断是否是被动启动状态
  15. if (self.isLaunchedPassively) {
  16. eventProperties[@"$app_state"] = @"background";
  17. }
  18. // 设置事件属性
  19. event[@"propeerties"] = eventProperties;
  20. // 打印
  21. [self printEvent:event];
  22. [self.fileStroe saveEvent:event];
  23. }

第五步:测试验证

第六步:在文件中读取和删除事件数据

  1. @interface SensorsAnalyticsFileStore : NSObject
  2. /// 保存存储文件的路径
  3. @property (nonatomic, copy) NSString *filePath;
  4. /// 获取本地缓存的所有事件数据
  5. @property (nonatomic, copy, readonly) NSArray<NSDictionary *> *allEvents;
  6. /// 将事件保存到文件中
  7. /// @param event 事件数据
  8. - (void)saveEvent:(NSDictionary *)event;
  9. /// 根据数量删除本地保存的事件数据
  10. /// @param count 需要删除的事件数量
  11. - (void)deleteEventsForCount:(NSInteger)count;
  12. @end
  1. - (void)readAllEventsFromFilePath:(NSString *)filePath {
  2. NSData *data = [NSData dataWithContentsOfFile:filePath];
  3. if (data) {
  4. // 解析在文件中读取 JSON 数据
  5. NSMutableArray *allEvents = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
  6. // 将文件中的数据读取到内存中
  7. self.events = allEvents ?: [NSMutableArray array];
  8. } else {
  9. self.events = [NSMutableArray array];
  10. }
  11. }
  12. - (NSArray<NSDictionary *> *)allEvents {
  13. return [self.events copy];
  14. }
  15. - (void)deleteEventsForCount:(NSInteger)count {
  16. // 删除前 count 条事件数据
  17. [self.events removeObjectsInRange:NSMakeRange(0, count)];
  18. // 将删除后剩余的事件数据保存到文件中
  19. [self writeEventsToFile];
  20. }

2.2 优化

​ 通过上面实现文件缓存存在两个非常明细的问题。

(1)如果在主线程中触发事件,那么读取事件、保存事件及删除事件都在主线程中运行,会出现所谓的 “卡主线程”问题。

(2)在无网环境下,如果在文件中缓存了大量的事件数据,会导致内存占用过大,影响应用程序性能。

2.2.1 多线程优化

​ 解决 “卡主线程” 问题的方法主要是把处理文件的逻辑都放到多线程中运行。

第一步:在 SensorsAnalyticsFileStore.m 文件中新增一个 dispatch_queue_t 类型的属性 queue, 并在 -init 方法中进行初始化

  1. @interface SensorsAnalyticsFileStore()
  2. /// 事件数据
  3. @property (nonatomic, strong) NSMutableArray<NSDictionary *> *events;
  4. /// 串行队列
  5. @property (nonatomic, strong) dispatch_queue_t queue;
  6. @end
  7. @implementation SensorsAnalyticsFileStore
  8. - (instancetype)init {
  9. self = [super init];
  10. if (self) {
  11. // 初始化默认的事件数据存储地址
  12. _filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultFileName];
  13. // 初始化队列的唯一标识
  14. NSString *label = [NSString stringWithFormat:@"cn.sensorsdata.serialQueue.%p", self];
  15. // 创建一个 serial 类型的 queue,即 FIFO
  16. _queue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL);
  17. _maxLocalEventCount = 1000;
  18. // 初始化事件数据,从文件路径中读取数据
  19. [self readAllEventsFromFilePath:_filePath];
  20. }
  21. return self;
  22. }
  23. @end

第二步:使用 dispatch_async 函数优化 - saveEvent: 、- readAllEventsFromFilePath: 及 - deleteEventsForCount: 方法,使用 dispatch_sync 函数优化 - allEvents 方法

  1. //
  2. // SensorsAnalyticsFileStore.m
  3. // SensorsSDK
  4. //
  5. // Created by 任伟 on 2022/4/12.
  6. //
  7. #import "SensorsAnalyticsFileStore.h"
  8. static NSString * const SensorsAnalyticsDefaultFileName = @"SensorsAnalytics.plist";
  9. @interface SensorsAnalyticsFileStore()
  10. /// 事件数据
  11. @property (nonatomic, strong) NSMutableArray<NSDictionary *> *events;
  12. /// 串行队列
  13. @property (nonatomic, strong) dispatch_queue_t queue;
  14. @end
  15. @implementation SensorsAnalyticsFileStore
  16. - (instancetype)init {
  17. self = [super init];
  18. if (self) {
  19. // 初始化默认的事件数据存储地址
  20. _filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultFileName];
  21. // 初始化队列的唯一标识
  22. NSString *label = [NSString stringWithFormat:@"cn.sensorsdata.serialQueue.%p", self];
  23. // 创建一个 serial 类型的 queue,即 FIFO
  24. _queue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL);
  25. _maxLocalEventCount = 1000;
  26. // 初始化事件数据,从文件路径中读取数据
  27. [self readAllEventsFromFilePath:_filePath];
  28. }
  29. return self;
  30. }
  31. - (void)saveEvent:(NSDictionary *)event {
  32. dispatch_async(self.queue, ^{
  33. if (self.events.count >= _maxLocalEventCount) {
  34. [self.events removeObjectAtIndex:0];
  35. }
  36. // 在数组中直接添加事件数据
  37. [self.events addObject:event];
  38. // 将事件数据保存在文件中
  39. [self writeEventsToFile];
  40. });
  41. }
  42. - (void)writeEventsToFile {
  43. NSError *error = nil;
  44. // 将字典数据解析成 JSON 数据
  45. NSData *data = [NSJSONSerialization dataWithJSONObject:self.events options:NSJSONWritingPrettyPrinted error:&error];
  46. if (error) {
  47. return NSLog(@"The JSON object`s serialization error: %@", error);
  48. }
  49. // 将数据写入到文件
  50. [data writeToFile:self.filePath atomically:YES];
  51. }
  52. - (void)readAllEventsFromFilePath:(NSString *)filePath {
  53. dispatch_async(self.queue, ^{
  54. NSData *data = [NSData dataWithContentsOfFile:filePath];
  55. if (data) {
  56. // 解析在文件中读取 JSON 数据
  57. NSMutableArray *allEvents = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
  58. // 将文件中的数据读取到内存中
  59. self.events = allEvents ?: [NSMutableArray array];
  60. } else {
  61. self.events = [NSMutableArray array];
  62. }
  63. });
  64. }
  65. - (NSArray<NSDictionary *> *)allEvents {
  66. __block NSArray<NSDictionary *> *allEvents = nil;
  67. dispatch_sync(self.queue, ^{
  68. allEvents = [self.events copy];
  69. })
  70. return allEvents;
  71. }
  72. - (void)deleteEventsForCount:(NSInteger)count {
  73. dispatch_async(self.queue, ^{
  74. // 删除前 count 条事件数据
  75. [self.events removeObjectsInRange:NSMakeRange(0, count)];
  76. // 将删除后剩余的事件数据保存到文件中
  77. [self writeEventsToFile];
  78. });
  79. }
  80. @end
2.2.2 内存优化

​ 设置一个本地可缓存的最大事件条数,当本地已经缓存到事件条数超过本地可缓存最大事件条数时,删除最旧的事件数据。以保证最新的事件数据可以被缓存。

第一步:在 SensorsAnalyticsFileStore.h 文件中新增 maxLocalEventCount 属性, 并在 - init 方法中进行初始化,默认设置 1000 条数。

  1. /// 本地可最大缓存事件条数
  2. @property (nonatomic) NSUInteger maxLocalEventCount;
  1. - (instancetype)init {
  2. self = [super init];
  3. if (self) {
  4. // 初始化默认的事件数据存储地址
  5. _filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultFileName];
  6. // 初始化队列的唯一标识
  7. NSString *label = [NSString stringWithFormat:@"cn.sensorsdata.serialQueue.%p", self];
  8. // 创建一个 serial 类型的 queue,即 FIFO
  9. _queue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL);
  10. _maxLocalEventCount = 1000;
  11. // 初始化事件数据,从文件路径中读取数据
  12. [self readAllEventsFromFilePath:_filePath];
  13. }
  14. return self;
  15. }

第二步:在 - saveEvent: 方法插入数据之前,先判断已缓存的事件条数是否超过了本地可缓存的事件条数,如果已经超过,则删除最旧的事件

  1. - (void)saveEvent:(NSDictionary *)event {
  2. dispatch_async(self.queue, ^{
  3. if (self.events.count >= _maxLocalEventCount) {
  4. [self.events removeObjectAtIndex:0];
  5. }
  6. // 在数组中直接添加事件数据
  7. [self.events addObject:event];
  8. // 将事件数据保存在文件中
  9. [self writeEventsToFile];
  10. });
  11. }

2.3 总结

​ 我们可以使用文件缓存实现事件数据的持久化操作。

首先,主要实现了一下三个功能:

  • 保存事件
  • 获取本地缓存的所有事件
  • 删除事件

然后有进行了两项优化

  • 多线程优化
  • 内存优化

​ 文件缓存相对来说还是比较简单,主要操作就是写文件和读取文件。每次写入的 数据量越大,文件缓存的性能越好。

​ 当然,文件缓存是不够灵活的,我们很难使用更细的颗粒去操作数据。比如很难对某一条数据进行读写操作。

三、数据库缓存

​ 在 iOS 应用程序中,使用的数据库一般是 SQLite 数据库,SQLite 是轻量级数据库,数据存储简单高效,使用也非常简单,只是需要在项目中添加 libssqlite3.0 依赖,并在使用的时候引入 sqlite3.h 头文件即可。

3.1 实现步骤

第一步:创建 SensorsAnalyticsDatabase 工具类

  1. //
  2. // SensorsAnalyticsDatabase.h
  3. // SensorsSDK
  4. //
  5. // Created by 任伟 on 2022/4/13.
  6. //
  7. #import <Foundation/Foundation.h>
  8. NS_ASSUME_NONNULL_BEGIN
  9. @interface SensorsAnalyticsDatabase : NSObject
  10. /// 数据库文件的路径
  11. @property (nonatomic, copy, readonly) NSString *filePath;
  12. //+ (instancetype)new NS_UNAVAILABLE;
  13. //- (instancetype)init NS_UNAVAILABLE;
  14. /// 初始化方法
  15. /// @param filePath 数据库路径,如果是nil, 使用默认路径
  16. - (instancetype)initWithFilePath:(nullable NSString *)filePath NS_DESIGNATED_INITIALIZER;
  17. /// 同步向数据库插入事件数据
  18. /// @param event 事件
  19. - (void)insertEvent: (NSDictionary *) event;
  20. /// 从数据库中获取事件数据
  21. /// @param count 获取事件数据条数
  22. - (NSArray<NSString *> *)selectEventsForCount:(NSInteger)count;
  23. /// 从数据库中删除一定数量的事件数据
  24. /// @param count 需要删除的事件条数
  25. - (BOOL)deleteEventsForCount:(NSInteger)count;
  26. @end
  27. NS_ASSUME_NONNULL_END
  1. //
  2. // SensorsAnalyticsDatabase.m
  3. // SensorsSDK
  4. //
  5. // Created by 任伟 on 2022/4/13.
  6. //
  7. #import "SensorsAnalyticsDatabase.h"
  8. #import <sqlite3.h>
  9. static NSString * const SensorsAnalyticsDefaultDatabaseName = @"SensorsAnalyticsDatabase.sqlite";
  10. @interface SensorsAnalyticsDatabase()
  11. /// 数据库文件的路径
  12. @property (nonatomic, copy) NSString *filePath;
  13. /// 数据库私有属性
  14. @property (nonatomic) sqlite3 *database;
  15. /// 串行队列
  16. @property (nonatomic, strong) dispatch_queue_t queue;
  17. @end
  18. @implementation SensorsAnalyticsDatabase {
  19. sqlite3 *_database;
  20. }
  21. - (instancetype)init {
  22. return [self initWithFilePath:nil];
  23. }
  24. - (instancetype)initWithFilePath:(NSString *)filePath {
  25. self = [super init];
  26. if (self) {
  27. _filePath = filePath ?: [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultDatabaseName];
  28. // 初始化队列的唯一标识
  29. NSString *label = [NSString stringWithFormat:@"cn.sensorsdata.serialQueue.%p", self];
  30. // 创建一个 serial 类型的 queue,即 FIFO
  31. _queue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL);
  32. // 打开数据库
  33. [self open];
  34. }
  35. return self;
  36. }
  37. - (void)open {
  38. dispatch_async(self.queue, ^{
  39. // 初始化 SQLite 库
  40. if (sqlite3_initialize() != SQLITE_OK) {
  41. return;
  42. }
  43. // 打开数据库,获取数据库指针
  44. if (sqlite3_open_v2([self.filePath UTF8String], &(self->_database), SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL) != SQLITE_OK) {
  45. return NSLog(@"SQLite stmt prepare error: %s", sqlite3_errmsg(self.database));
  46. }
  47. char *error;
  48. // 创建数据库表的 SQL 语句
  49. // NSString *sql = @"CREATE TABLE IF NOT EXISTS events(id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL UNIQUE);";
  50. NSString *sql = @"CREATE TABLE IF NOT EXISTS events (id integer PRIMARY KEY AUTOINCREMENT, event BLOB);";
  51. // 运行创建表格的 SQL 语句
  52. if (sqlite3_exec(self.database, [sql UTF8String], NULL, NULL, &error) != SQLITE_OK) {
  53. return NSLog(@"Create events failure %s", error);
  54. }
  55. });
  56. }
  57. - (void)insertEvent:(NSDictionary *)event {
  58. dispatch_async(self.queue, ^{
  59. // 自定义 SQLite Statement
  60. sqlite3_stmt *stmt;
  61. // 插入语句
  62. NSString *sql = @"INSERT INTO events (event) values (?)";
  63. // 准备执行 SQL 语句,获取 sqlite3_stmt
  64. if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &stmt, NULL) != SQLITE_OK) {
  65. // 准备执行 SQL 语句失败,打印 log 返回失败 NO
  66. return NSLog(@"SQLite stmt prepare error: %s", sqlite3_errmsg(self.database));
  67. }
  68. NSError *error;
  69. // 将 event 转换成 JSON 数据
  70. NSData *data = [NSJSONSerialization dataWithJSONObject:event options:NSJSONWritingPrettyPrinted error:&error];
  71. if (error) {
  72. return NSLog(@"The JSON object`s serialization error: %@", error);
  73. }
  74. // 将JSON数据与 stmt 绑定
  75. sqlite3_bind_blob(stmt, 1, data.bytes, (int)data.length, SQLITE_TRANSIENT);
  76. // 执行 stmt
  77. if (sqlite3_step(stmt) != SQLITE_DONE) {
  78. // 执行失败,打印log,返回失败(NO)
  79. return NSLog(@"Insert event into events error");
  80. }
  81. });
  82. }
  83. - (NSArray<NSString *> *)selectEventsForCount:(NSInteger)count {
  84. // 初始化数组,用于存储查询到的事件数据
  85. NSMutableArray<NSString *> *events = [NSMutableArray arrayWithCapacity:count];
  86. dispatch_sync(self.queue, ^{
  87. // 自定义 SQLite Statement
  88. sqlite3_stmt *stmt;
  89. // 查询语句
  90. NSString *sql = [NSString stringWithFormat:@"SELECT id, event FROM events ORDER BY id ASC LIMIT %lu", (unsigned long)count];
  91. // 准备执行 SQL 语句,获取sqlite3——stmt
  92. if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &stmt, NULL) != SQLITE_OK) {
  93. // 准备执行 SQL 语句失败,打印log返回失败(no)
  94. return NSLog(@"SQLite stmt prepare error: %s,", sqlite3_errmsg(self.database));
  95. }
  96. // 执行 SQL 语句
  97. while (sqlite3_step(stmt) == SQLITE_ROW) {
  98. // 将当前查询的这条数据转换成 NSData 对象
  99. NSData *data = [[NSData alloc] initWithBytes:sqlite3_column_blob(stmt, 1) length:sqlite3_column_bytes(stmt, 1)];
  100. // 将查询到的时间数据转换成JSON字符串
  101. NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  102. #ifdef DUBUG
  103. NSLog(@"%@", jsonString);
  104. #endif
  105. // 将JSON字符串添加到数组中
  106. [events addObject:jsonString];
  107. }
  108. });
  109. return events;
  110. }
  111. - (BOOL)deleteEventsForCount:(NSInteger)count {
  112. __block BOOL success = YES;
  113. dispatch_sync(self.queue, ^{
  114. // 删除语句
  115. NSString *sql = [NSString stringWithFormat:@"DELETE FROM events WHERE id IN (SELECT id FROM events ORDER BY id ASC LIMIT %lu);", (unsigned long)count];
  116. char *errmsg;
  117. //执行删除语句
  118. if (sqlite3_exec(self.database, sql.UTF8String, NULL, NULL, &errmsg) != SQLITE_OK) {
  119. success = NO;
  120. return NSLog(@"Failed to delete record msg=%s", errmsg);
  121. }
  122. });
  123. return success;
  124. }
  125. @end

第二步:在 SensorsAnalyticsSDK.m 文件中新增 SensorsAnalyticsDatabase 类型私有属性 database,并在 -init 方法中进行初始化

  1. - (instancetype)init {
  2. self = [super init];
  3. if (self) {
  4. _automaticProperties = [self collectAutomaticProperties];
  5. // 设置是否需是被动启动标记
  6. _launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
  7. _loginId = [[NSUserDefaults standardUserDefaults] objectForKey:SensorsAnalyticsLoginId];
  8. _trackTimer = [NSMutableDictionary dictionary];
  9. _enterBackgroundTrackTimerEvents = [NSMutableArray array];
  10. _fileStroe = [[SensorsAnalyticsFileStore alloc] init];
  11. _database = [[SensorsAnalyticsDatabase alloc] init];
  12. // 添加应用程序状态监听
  13. [self setupListeners];
  14. }
  15. return self;
  16. }

第三步:修改 -track: properties: 的数据存储方式

  1. - (void)track:(NSString *)eventName properties:(nullable NSDictionary<NSString *, id> *)properties {
  2. NSMutableDictionary *event = [NSMutableDictionary dictionary];
  3. // 设置事件 distinct_id 字段,用于唯一标识一个用户
  4. event[@"distinct_id"] = self.loginId ?: self.anonymousId;
  5. // 设置事件名称
  6. event[@"event"] = eventName;
  7. // 事件发生的时间戳,单位毫秒
  8. event[@"time"] = [NSNumber numberWithLong:NSDate.date.timeIntervalSince1970 *1000];
  9. NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
  10. // 添加预置属性
  11. [eventProperties addEntriesFromDictionary:self.automaticProperties];
  12. // 添加自定义属性
  13. [eventProperties addEntriesFromDictionary:properties];
  14. // 判断是否是被动启动状态
  15. if (self.isLaunchedPassively) {
  16. eventProperties[@"$app_state"] = @"background";
  17. }
  18. // 设置事件属性
  19. event[@"propeerties"] = eventProperties;
  20. // 打印
  21. [self printEvent:event];
  22. // [self.fileStroe saveEvent:event];
  23. [self.database insertEvent:event];
  24. }

第四步:测试验证(和文件存储验证方式一样)

3.2 优化

​ 需要优化的内容:

在每次插入和查询数据的时候,都会执行 “准备执行SQL的语句”的操作,比较浪费资源

在查询和删除操作时,如果数据表中没有存储任何的数据,其实无须执行 SQL 语句

(1)缓存 sqlite3_stmt
  1. static sqlite3_stmt *insertStmt = NULL;
  2. - (void)insertEvent:(NSDictionary *)event {
  3. dispatch_async(self.queue, ^{
  4. if (insertStmt) {
  5. // 重置插入语句,重置之后可重新绑定数据
  6. sqlite3_reset(insertStmt);
  7. } else {
  8. // 插入语句
  9. NSString *sql = @"INSERT INTO events (event) values (?)";
  10. // 准备执行 SQL 语句,获取 sqlite3_stmt
  11. if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &insertStmt, NULL) != SQLITE_OK) {
  12. // 准备执行 SQL 语句失败,打印 log 返回失败 NO
  13. return NSLog(@"SQLite stmt prepare error: %s", sqlite3_errmsg(self.database));
  14. }
  15. }
  16. NSError *error;
  17. // 将 event 转换成 JSON 数据
  18. NSData *data = [NSJSONSerialization dataWithJSONObject:event options:NSJSONWritingPrettyPrinted error:&error];
  19. if (error) {
  20. return NSLog(@"The JSON object`s serialization error: %@", error);
  21. }
  22. // 将JSON数据与 insertStmt 绑定
  23. sqlite3_bind_blob(insertStmt, 1, data.bytes, (int)data.length, SQLITE_TRANSIENT);
  24. // 执行 stmt
  25. if (sqlite3_step(insertStmt) != SQLITE_DONE) {
  26. // 执行失败,打印log,返回失败(NO)
  27. return NSLog(@"Insert event into events error");
  28. }
  29. });
  30. }
  1. // 最后一次查询下的事件数量
  2. static NSUInteger lastSelectEventCount = 50;
  3. static sqlite3_stmt *selectStmt = NULL;
  4. - (NSArray<NSString *> *)selectEventsForCount:(NSInteger)count {
  5. // 初始化数组,用于存储查询到的事件数据
  6. NSMutableArray<NSString *> *events = [NSMutableArray arrayWithCapacity:count];
  7. dispatch_sync(self.queue, ^{
  8. if (count != lastSelectEventCount) {
  9. lastSelectEventCount = count;
  10. selectStmt = NULL;
  11. }
  12. if (selectStmt) {
  13. // 重置插入语句,重置之后可重新查询数据
  14. sqlite3_reset(selectStmt);
  15. } else {
  16. // 查询语句
  17. NSString *sql = [NSString stringWithFormat:@"SELECT id, event FROM events ORDER BY id ASC LIMIT %lu", (unsigned long)count];
  18. // 准备执行 SQL 语句,获取sqlite3——stmt
  19. if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &selectStmt, NULL) != SQLITE_OK) {
  20. // 准备执行 SQL 语句失败,打印log返回失败(no)
  21. return NSLog(@"SQLite stmt prepare error: %s,", sqlite3_errmsg(self.database));
  22. }
  23. }
  24. // 执行 SQL 语句
  25. while (sqlite3_step(selectStmt) == SQLITE_ROW) {
  26. // 将当前查询的这条数据转换成 NSData 对象
  27. NSData *data = [[NSData alloc] initWithBytes:sqlite3_column_blob(selectStmt, 1) length:sqlite3_column_bytes(stmt, 1)];
  28. // 将查询到的时间数据转换成JSON字符串
  29. NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  30. #ifdef DUBUG
  31. NSLog(@"%@", jsonString);
  32. #endif
  33. // 将JSON字符串添加到数组中
  34. [events addObject:jsonString];
  35. }
  36. });
  37. return events;
  38. }
(2)缓存事件总条数

​ 添加一个方法用于查询数据库已经存储事件条数,新增一个 eventCount 属性,初始化时,他的数值就是当前数据库已经存储事件条数,每次成功插入一条数据的时候值对应的加1,在删除数据的时候减去相应删除的数据条数,这样就保证 eventCount 和本地数据存储的事件条数一致,减少查询次数。

第一步:在 SensorsAnalyticsDatabase.h 中新增 eventCount 属性

  1. /// 本地事件存储总量
  2. @property (nonatomic) NSUInteger eventCount;

第二步:在 SensorsAnalyticsDatabase.m 文件中新增私有方法 - queryLocalDatabaseEventCount,查询数据库中已经缓存事件数。

  1. // 查询数据库中已经缓存事件的条数
  2. - (void)queryLocalDatabaseEventCount {
  3. dispatch_async(self.queue, ^{
  4. // 查询语句
  5. NSString *sql = @"SELECT count(*) FORM events";
  6. sqlite3_stmt *stmt = NULL;
  7. // 准备执行SQL语句,获取 sqlite3_stmt
  8. if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &stmt, NULL) != SQLITE_OK) {
  9. // 准备执行SQL语句失败,打印log返回失败 NO
  10. return NSLog(@"SQLite stmt prepare error: %s", sqlite3_errmsg(self.database));
  11. }
  12. while (sqlite3_step(stmt) == SQLITE_ROW) {
  13. self.eventCount = sqlite3_column_int(stmt, 0);
  14. }
  15. });
  16. }

第三步 :在 - initWithFilePath: 初始化方法中调用 - queryLocalDatabaseEventCount,初始化 eventCount

  1. - (instancetype)initWithFilePath:(NSString *)filePath {
  2. self = [super init];
  3. if (self) {
  4. _filePath = filePath ?: [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultDatabaseName];
  5. // 初始化队列的唯一标识
  6. NSString *label = [NSString stringWithFormat:@"cn.sensorsdata.serialQueue.%p", self];
  7. // 创建一个 serial 类型的 queue,即 FIFO
  8. _queue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL);
  9. // 打开数据库
  10. [self open];
  11. [self queryLocalDatabaseEventCount];
  12. }
  13. return self;
  14. }

第四步:优化 - insertEvent: 方法,事件插入成功,事件数量 eventCount 加 1

  1. static sqlite3_stmt *insertStmt = NULL;
  2. - (void)insertEvent:(NSDictionary *)event {
  3. dispatch_async(self.queue, ^{
  4. if (insertStmt) {
  5. // 重置插入语句,重置之后可重新绑定数据
  6. sqlite3_reset(insertStmt);
  7. } else {
  8. // 插入语句
  9. NSString *sql = @"INSERT INTO events (event) values (?)";
  10. // 准备执行 SQL 语句,获取 sqlite3_stmt
  11. if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &insertStmt, NULL) != SQLITE_OK) {
  12. // 准备执行 SQL 语句失败,打印 log 返回失败 NO
  13. return NSLog(@"SQLite stmt prepare error: %s", sqlite3_errmsg(self.database));
  14. }
  15. }
  16. NSError *error;
  17. // 将 event 转换成 JSON 数据
  18. NSData *data = [NSJSONSerialization dataWithJSONObject:event options:NSJSONWritingPrettyPrinted error:&error];
  19. if (error) {
  20. return NSLog(@"The JSON object`s serialization error: %@", error);
  21. }
  22. // 将JSON数据与 insertStmt 绑定
  23. sqlite3_bind_blob(insertStmt, 1, data.bytes, (int)data.length, SQLITE_TRANSIENT);
  24. // 执行 stmt
  25. if (sqlite3_step(insertStmt) != SQLITE_DONE) {
  26. // 执行失败,打印log,返回失败(NO)
  27. return NSLog(@"Insert event into events error");
  28. }
  29. // 数据插入成功 事件数量加1
  30. self.eventCount ++;
  31. });
  32. }

第五步:优化 - deleteEventsForCount: 方法,当 eventCount 为 0 时,直接返回;当数据删除成功时,事件数量减去相应的删除条数

  1. - (BOOL)deleteEventsForCount:(NSInteger)count {
  2. __block BOOL success = YES;
  3. dispatch_sync(self.queue, ^{
  4. // 当本地事件数量为 0 时,直接返回
  5. if (self.eventCount == 0) {
  6. return;
  7. }
  8. // 删除语句
  9. NSString *sql = [NSString stringWithFormat:@"DELETE FROM events WHERE id IN (SELECT id FROM events ORDER BY id ASC LIMIT %lu);", (unsigned long)count];
  10. char *errmsg;
  11. //执行删除语句
  12. if (sqlite3_exec(self.database, sql.UTF8String, NULL, NULL, &errmsg) != SQLITE_OK) {
  13. success = NO;
  14. return NSLog(@"Failed to delete record msg=%s", errmsg);
  15. }
  16. self.eventCount = self.eventCount < count ? 0 : self.eventCount - count;
  17. });
  18. return success;
  19. }

第六步:优化 - selectEventsForCount: 方法,当 eventCount 为 0 时,直接返回

  1. // 最后一次查询下的事件数量
  2. static NSUInteger lastSelectEventCount = 50;
  3. static sqlite3_stmt *selectStmt = NULL;
  4. - (NSArray<NSString *> *)selectEventsForCount:(NSInteger)count {
  5. // 初始化数组,用于存储查询到的事件数据
  6. NSMutableArray<NSString *> *events = [NSMutableArray arrayWithCapacity:count];
  7. dispatch_sync(self.queue, ^{
  8. // 当本地事件数量为 0 ,直接返回
  9. if (self.eventCount == 0) {
  10. return;
  11. }
  12. if (count != lastSelectEventCount) {
  13. lastSelectEventCount = count;
  14. selectStmt = NULL;
  15. }
  16. if (selectStmt) {
  17. // 重置插入语句,重置之后可重新查询数据
  18. sqlite3_reset(selectStmt);
  19. } else {
  20. // 查询语句
  21. NSString *sql = [NSString stringWithFormat:@"SELECT id, event FROM events ORDER BY id ASC LIMIT %lu", (unsigned long)count];
  22. // 准备执行 SQL 语句,获取sqlite3——stmt
  23. if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &selectStmt, NULL) != SQLITE_OK) {
  24. // 准备执行 SQL 语句失败,打印log返回失败(no)
  25. return NSLog(@"SQLite stmt prepare error: %s,", sqlite3_errmsg(self.database));
  26. }
  27. }
  28. // 执行 SQL 语句
  29. while (sqlite3_step(selectStmt) == SQLITE_ROW) {
  30. // 将当前查询的这条数据转换成 NSData 对象
  31. NSData *data = [[NSData alloc] initWithBytes:sqlite3_column_blob(selectStmt, 1) length:sqlite3_column_bytes(stmt, 1)];
  32. // 将查询到的时间数据转换成JSON字符串
  33. NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  34. #ifdef DUBUG
  35. NSLog(@"%@", jsonString);
  36. #endif
  37. // 将JSON字符串添加到数组中
  38. [events addObject:jsonString];
  39. }
  40. });
  41. return events;
  42. }

3.3 总结

​ 通过上面我们实现了数据库缓存事件数据,并实现了如下功能

  • 插入数据
  • 查询数据
  • 删除数据

​ 然后对数据缓存性能进行了优化。对于文件缓存来说,数据库缓存更加灵活,可以实现对单条数据的查询、插入和删除操作,同时调试也更容易。SQLite 数据库也有极高的性能,特别是对单条数据的操作,性能明显由于文件缓存。

iOS全埋点解决方案-数据存储的更多相关文章

  1. iOS全埋点解决方案-手势采集

    前言 ​ 随着科技以及业务的发展,手势的应用也越来越普及,因此对于数据采集,我们要考虑如果通过全埋点来实现手势的采集. 一.手势识别器 ​ 苹果为了降低开发者在手势事件处理方面的开发难度,定义了一个抽 ...

  2. iOS全埋点解决方案-UITableView和UICollectionView点击事件

    前言 在 $AppClick 事件采集中,还有两个比较特殊的控件: UITableView •UICollectionView 这两个控件的点击事件,一般指的是点击 UITableViewCell 和 ...

  3. iOS全埋点解决方案-采集奔溃

    前言 ​ 采集应用程序奔溃信息,主要分为以下两种场景: ​ NSException 异常 ​ Unix 信号异常 一.NSException 异常 ​ NSException 异常是 Objectiv ...

  4. iOS全埋点解决方案-应用退出和启动

    前言 ​ 通过应用程序退出事件,可以分析应用程序的平均使用时长:通过应用程序的启动事件,可以分析日活和新增.我们可以通过全埋点方式 SDK 实现应用程序的退出和启动事件. 一.全埋点的简介 ​ 目前. ...

  5. iOS全埋点解决方案-界面预览事件

    前言 ​ 我们先了解 UIViewController 生命周期相关的内容和 iOS 的"黑魔法" Method Swizzling.然后再了解页面浏览事件($AppViewScr ...

  6. iOS全埋点解决方案-控件点击事件

    前言 ​ 我们主要介绍如何实现控件点击事件($AppClick)的全埋点.在介绍如何实现之前,我们需要先了解一下,在 UIKit 框架下,处理点击或拖动事件的 Target-Action 设计模式. ...

  7. iOS全埋点解决方案-时间相关

    前言 ​ 我们使用"事件模型( Event 模型)"来描述用户的各种行为,事件模型包括事件( Event )和用户( User )两个核心实体.我们在描述用户行为时,往往只需要描述 ...

  8. iOS全埋点解决方案-APP和H5打通

    前言 ​ 所谓的 APP 和 H5 打通,是指 H5 集成 JavaScript 数据采集 SDK 后,H5 触发的事件不直接同步给服务器,而是先发给 APP 端的数据采集 SDK,经过 APP 端数 ...

  9. IOS开发数据存储篇—IOS中的几种数据存储方式

    IOS开发数据存储篇—IOS中的几种数据存储方式 发表于2016/4/5 21:02:09  421人阅读 分类: 数据存储 在项目开发当中,我们经常会对一些数据进行本地缓存处理.离线缓存的数据一般都 ...

随机推荐

  1. 串联型PID,并联型PID与标准型PID简要说明

    PID广泛应用于工业生产各个环节,然而对于不同PID结构会有一些差异,导致在调参时若按照常规的经验调试,结果将会有非常大的不同. 串联型PID(Serial PID) 串联型PID的三个环节由比例,积 ...

  2. 220v-5v稳压电路

    5V整流电路原理 先对电路进行整流 整流电路:利用单向导电器件将交流电转换成脉动直流电路,再用电容进行滤波 滤波电路:利用储能元件(电感或电容)把脉动直流电转换成比较平坦的直流电,然后对电路进行稳压 ...

  3. Android Studio安装问题

    安装问题可以参考:https://blog.csdn.net/y74364/article/details/96121530 但是gradle安装缓慢,需要FQ.有加速器FQ的可以开加速器安装,没有的 ...

  4. JavaScript 遍历对象、数组总结

    在日常工作过程中,我们对于javaScript遍历对象.数组的操作是十分的频繁的,今天抽空把经常用到的方法小结一下,方便今后参考使用!   javaScript遍历对象总结     1.使用Objec ...

  5. php 实验一 网页设计

    实验目的: 1.  能够对整个页面进行html结构设计. 2.  掌握CSS+DIV的应用. 实验内容及要求: ***个人博客网页 参考Internet网上的博客网站,设计自己的个人网页,主要包括:图 ...

  6. 关于mysql使用utf8编码在cmd窗口无法添加中文数据的问题以及解决 方法二

    如果非要用cmd窗口的话,那么可以加这句话,set names gbk:

  7. HashMap和ConcurrentHashMap的原理和实现

    一.线程不安全的HashMap 多线程环境下,使用HashMap进行put操作会引起死循环(jdk1.7 Entry链表形成环形数据结构),导致CPU利用率接近100%. 结构:数组 table[]+ ...

  8. Java报错: A component required a bean of type 'com.sirifeng.testmybatis.mapper.BookMapper' that could not be found.

    在学习Spring-boot-mybatis时,报错A component required a bean of type 'com.sirifeng.testmybatis.mapper.BookM ...

  9. Thinkphp3.2.3 where注入 浅分析漏洞原理及修复

    0x01引子 0x02分析 找到截断方法 找到_parseType的入口 找到生成sql语句的代码 0x03 poc链 0x04 利用示范 payload: http://localhost:3000 ...

  10. For-Each循环(增强型For循环)

    public class Demo077 { public static void main(String[] args) { int[] array ={11,2}; System.out.prin ...