SDK 需要把事件数据缓冲到本地,待符合一定策略再去同步数据。
在 iOS 应用程序中,从 “数据缓冲在哪里” 这个纬度看,缓冲一般分两种类型。
内存缓冲是将数据缓冲在内存中,供应用程序直接读取和使用。优点是读取速度快。缺点是由于内存资源有限,应用程序在系统中申请的内存,会随着应用生命周期结束而被释放,会导致内存中的数据丢失,因此将事件数据缓冲到内存中不是最佳选择。
磁盘缓冲是将数据缓冲到磁盘空间中,其特点正好和磁盘缓冲相反。磁盘缓冲容量打,但是读写速度对于内存缓冲要慢点。不过磁盘缓冲可以持久化存储,不受应用程序生命周期影响。因为,将数据保存在磁盘中,丢失的风险比较低。即使磁盘缓冲数据速度较慢,但综合考虑,磁盘缓冲是缓冲事件数据最优的选择。
iOS 系统为了保证系统的安全性,采用了沙盒机制(即每个应用程序都有自己的一个独立存储空间)。其原理就是通过重定向技术,把应用程序生成和修改的文件重定向到自身的文件中。因此,在 iOS 应用程序里,磁盘缓存的数据一般都存储在沙盒中。
我们可以通过下面的方式获取沙盒路径:
// 获取沙盒主目录路径
NSString *homeDir = NSHomeDirectory();
在模拟上,输出沙盒路径示例如下:
/Users/renwei/Library/Developer/CoreSimulator/Devices/B1D7EC3E-BE72-4F8D-A4EF-E3D6316827CF/data/Containers/Data/Application/229B24A6-E13D-4DE6-9B52-363E832F9717
沙盒的根目录下有三个常用的文件夹:
(1)Document 文件夹
在 Document 文件夹中,保存的一般是应用程序本身产生的数据。
获取 Document 文件夹路径的方法:
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask , YES).lastObject;
/Users/renwei/Library/Developer/CoreSimulator/Devices/B1D7EC3E-BE72-4F8D-A4EF-E3D6316827CF/data/Containers/Data/Application/86212089-1D48-4B92-A919-AB87D3683191/Documents
(2) Library 文件夹
获取 Library 文件夹路径方法:
NSString *path = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask , NO).lastObject;
/Users/renwei/Library/Developer/CoreSimulator/Devices/B1D7EC3E-BE72-4F8D-A4EF-E3D6316827CF/data/Containers/Data/Application/4BBA5D3E-0C75-4543-B831-AE3344DCC940/Library
在 Library 文件夹下有两个常用的子文件夹:
Caches 文件夹主要用来保存应用程序运行时产生的需要持久化的数据,需要应用程序复制删除。
获取 Caches 文件夹路径的方法
NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask , YES).lastObject;
/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 文件路径的方法:
NSString *path = NSTemporaryDirectory();
/Users/renwei/Library/Developer/CoreSimulator/Devices/B1D7EC3E-BE72-4F8D-A4EF-E3D6316827CF/data/Containers/Data/Application/8E8906B8-0CBC-4A83-A220-A09F397304CD/tmp/
通过上面综合对比发现,最适合缓存事件数据的地方,就是 Library 下 Caches 文件夹中。
在 iOS 应用程序中,一般通过两种方式进行磁盘缓存:
这两种方式都是可以实现数据采集 SDK 的缓冲机制。缓冲的策略即当事件发生后,先将事件数据存储在缓存中,待符合一定策略后从缓存中读取事件数据并进行同步,同步成功后,将已同步的事件从缓存中删除。
对于写入的性能,SQLite 数据库优于文件缓存.
对于读取的性能:如果单条数据小于 100KB,则 SQLite 数据库读取的速度更快。如果单条数据大于 100KB,则从文件中读取的速度更快。
因此,数据采集 SDK 一般都是使用 SQLite 数据库来缓存数据,这样可以拥有最佳的读写性能。如果希望采集更完整,更全面的信息,比如采集用户操作时当前截图的信息(一般超过100KB),文件缓存可能是最优的选择。
可以使用 NSKeyedArchiver 类将字典对象进行归档并写入文件,也可以使用 NSJSONSerialization 类把字典对象转成 JSON 格式字符串写入文件。
第一步:新建处理文件的工具类 SensorsAnalyticsFileStore ,在工具类中新增一个属性 filePath 用于保存存储文件的路径。在 SensorsAnalyticsFileStore 文件的 -init 方法中初始化 filePath 属性,我们默认在 Caches 目录下 SensorsAnalytics.plist 文件来缓存数据。
@interface SensorsAnalyticsFileStore : NSObject
/// 保存存储文件的路径
@property (nonatomic, copy) NSString *filePath;
@end
static NSString * const SensorsAnalyticsDefaultFileName = @"SensorsAnalytics.plist";
@implementation SensorsAnalyticsFileStore
- (instancetype)init {
self = [super init];
if (self) {
// 初始化默认的事件数据存储地址
_filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultFileName];
}
return self;
}
@end
第二步:我们使用 NSJSONSerialization 类将字典对象转换成 JSON 格式并写入文件。新增 - saveEvent: 方法用于事件数据写入文件,同时,新增 NSMutableArray<NSDictionary *> *events;并在 - init 方法中进行初始化
- (instancetype)init {
self = [super init];
if (self) {
// 初始化默认的事件数据存储地址
_filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultFileName];
// 初始化事件数据,从文件路径中读取数据
[self readAllEventsFromFilePath:_filePath];
}
return self;
}
- (void)saveEvent:(NSDictionary *)event {
// 在数组中直接添加事件数据
[self.events addObject:event];
// 将事件数据保存在文件中
[self writeEventsToFile];
}
- (void)writeEventsToFile {
NSError *error = nil;
// 将字典数据解析成 JSON 数据
NSData *data = [NSJSONSerialization dataWithJSONObject:self.events options:NSJSONWritingPrettyPrinted error:&error];
if (error) {
return NSLog(@"The JSON object`s serialization error: %@", error);
}
// 将数据写入到文件
[data writeToFile:self.filePath atomically:YES];
}
第三步:在 SensorsAnalyticsSDK.m 文件中新增一个 SensorsAnalyticsFileStore 类型属性 fileStroe,并在 - init 方法中进行初始化
#import "SensorsAnalyticsFileStore.h"
/// 文件缓存事件数据对象
@property (nonatomic, strong) SensorsAnalyticsFileStore *fileStroe;
- (instancetype)init {
self = [super init];
if (self) {
_automaticProperties = [self collectAutomaticProperties];
// 设置是否需是被动启动标记
_launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
_loginId = [[NSUserDefaults standardUserDefaults] objectForKey:SensorsAnalyticsLoginId];
_trackTimer = [NSMutableDictionary dictionary];
_enterBackgroundTrackTimerEvents = [NSMutableArray array];
_fileStroe = [[SensorsAnalyticsFileStore alloc] init];
// 添加应用程序状态监听
[self setupListeners];
}
return self;
}
第四步:修改 SensorsAnalyticsSDK 的类别 Track 中的 - track: properties: 方法。
- (void)track:(NSString *)eventName properties:(nullable NSDictionary<NSString *, id> *)properties {
NSMutableDictionary *event = [NSMutableDictionary dictionary];
// 设置事件 distinct_id 字段,用于唯一标识一个用户
event[@"distinct_id"] = self.loginId ?: self.anonymousId;
// 设置事件名称
event[@"event"] = eventName;
// 事件发生的时间戳,单位毫秒
event[@"time"] = [NSNumber numberWithLong:NSDate.date.timeIntervalSince1970 *1000];
NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
// 添加预置属性
[eventProperties addEntriesFromDictionary:self.automaticProperties];
// 添加自定义属性
[eventProperties addEntriesFromDictionary:properties];
// 判断是否是被动启动状态
if (self.isLaunchedPassively) {
eventProperties[@"$app_state"] = @"background";
}
// 设置事件属性
event[@"propeerties"] = eventProperties;
// 打印
[self printEvent:event];
[self.fileStroe saveEvent:event];
}
第五步:测试验证
第六步:在文件中读取和删除事件数据
@interface SensorsAnalyticsFileStore : NSObject
/// 保存存储文件的路径
@property (nonatomic, copy) NSString *filePath;
/// 获取本地缓存的所有事件数据
@property (nonatomic, copy, readonly) NSArray<NSDictionary *> *allEvents;
/// 将事件保存到文件中
/// @param event 事件数据
- (void)saveEvent:(NSDictionary *)event;
/// 根据数量删除本地保存的事件数据
/// @param count 需要删除的事件数量
- (void)deleteEventsForCount:(NSInteger)count;
@end
- (void)readAllEventsFromFilePath:(NSString *)filePath {
NSData *data = [NSData dataWithContentsOfFile:filePath];
if (data) {
// 解析在文件中读取 JSON 数据
NSMutableArray *allEvents = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
// 将文件中的数据读取到内存中
self.events = allEvents ?: [NSMutableArray array];
} else {
self.events = [NSMutableArray array];
}
}
- (NSArray<NSDictionary *> *)allEvents {
return [self.events copy];
}
- (void)deleteEventsForCount:(NSInteger)count {
// 删除前 count 条事件数据
[self.events removeObjectsInRange:NSMakeRange(0, count)];
// 将删除后剩余的事件数据保存到文件中
[self writeEventsToFile];
}
通过上面实现文件缓存存在两个非常明细的问题。
(1)如果在主线程中触发事件,那么读取事件、保存事件及删除事件都在主线程中运行,会出现所谓的 “卡主线程”问题。
(2)在无网环境下,如果在文件中缓存了大量的事件数据,会导致内存占用过大,影响应用程序性能。
解决 “卡主线程” 问题的方法主要是把处理文件的逻辑都放到多线程中运行。
第一步:在 SensorsAnalyticsFileStore.m 文件中新增一个 dispatch_queue_t 类型的属性 queue, 并在 -init 方法中进行初始化
@interface SensorsAnalyticsFileStore()
/// 事件数据
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *events;
/// 串行队列
@property (nonatomic, strong) dispatch_queue_t queue;
@end
@implementation SensorsAnalyticsFileStore
- (instancetype)init {
self = [super init];
if (self) {
// 初始化默认的事件数据存储地址
_filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultFileName];
// 初始化队列的唯一标识
NSString *label = [NSString stringWithFormat:@"cn.sensorsdata.serialQueue.%p", self];
// 创建一个 serial 类型的 queue,即 FIFO
_queue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL);
_maxLocalEventCount = 1000;
// 初始化事件数据,从文件路径中读取数据
[self readAllEventsFromFilePath:_filePath];
}
return self;
}
@end
第二步:使用 dispatch_async 函数优化 - saveEvent: 、- readAllEventsFromFilePath: 及 - deleteEventsForCount: 方法,使用 dispatch_sync 函数优化 - allEvents 方法
//
// SensorsAnalyticsFileStore.m
// SensorsSDK
//
// Created by 任伟 on 2022/4/12.
//
#import "SensorsAnalyticsFileStore.h"
static NSString * const SensorsAnalyticsDefaultFileName = @"SensorsAnalytics.plist";
@interface SensorsAnalyticsFileStore()
/// 事件数据
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *events;
/// 串行队列
@property (nonatomic, strong) dispatch_queue_t queue;
@end
@implementation SensorsAnalyticsFileStore
- (instancetype)init {
self = [super init];
if (self) {
// 初始化默认的事件数据存储地址
_filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultFileName];
// 初始化队列的唯一标识
NSString *label = [NSString stringWithFormat:@"cn.sensorsdata.serialQueue.%p", self];
// 创建一个 serial 类型的 queue,即 FIFO
_queue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL);
_maxLocalEventCount = 1000;
// 初始化事件数据,从文件路径中读取数据
[self readAllEventsFromFilePath:_filePath];
}
return self;
}
- (void)saveEvent:(NSDictionary *)event {
dispatch_async(self.queue, ^{
if (self.events.count >= _maxLocalEventCount) {
[self.events removeObjectAtIndex:0];
}
// 在数组中直接添加事件数据
[self.events addObject:event];
// 将事件数据保存在文件中
[self writeEventsToFile];
});
}
- (void)writeEventsToFile {
NSError *error = nil;
// 将字典数据解析成 JSON 数据
NSData *data = [NSJSONSerialization dataWithJSONObject:self.events options:NSJSONWritingPrettyPrinted error:&error];
if (error) {
return NSLog(@"The JSON object`s serialization error: %@", error);
}
// 将数据写入到文件
[data writeToFile:self.filePath atomically:YES];
}
- (void)readAllEventsFromFilePath:(NSString *)filePath {
dispatch_async(self.queue, ^{
NSData *data = [NSData dataWithContentsOfFile:filePath];
if (data) {
// 解析在文件中读取 JSON 数据
NSMutableArray *allEvents = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
// 将文件中的数据读取到内存中
self.events = allEvents ?: [NSMutableArray array];
} else {
self.events = [NSMutableArray array];
}
});
}
- (NSArray<NSDictionary *> *)allEvents {
__block NSArray<NSDictionary *> *allEvents = nil;
dispatch_sync(self.queue, ^{
allEvents = [self.events copy];
})
return allEvents;
}
- (void)deleteEventsForCount:(NSInteger)count {
dispatch_async(self.queue, ^{
// 删除前 count 条事件数据
[self.events removeObjectsInRange:NSMakeRange(0, count)];
// 将删除后剩余的事件数据保存到文件中
[self writeEventsToFile];
});
}
@end
设置一个本地可缓存的最大事件条数,当本地已经缓存到事件条数超过本地可缓存最大事件条数时,删除最旧的事件数据。以保证最新的事件数据可以被缓存。
第一步:在 SensorsAnalyticsFileStore.h 文件中新增 maxLocalEventCount 属性, 并在 - init 方法中进行初始化,默认设置 1000 条数。
/// 本地可最大缓存事件条数
@property (nonatomic) NSUInteger maxLocalEventCount;
- (instancetype)init {
self = [super init];
if (self) {
// 初始化默认的事件数据存储地址
_filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultFileName];
// 初始化队列的唯一标识
NSString *label = [NSString stringWithFormat:@"cn.sensorsdata.serialQueue.%p", self];
// 创建一个 serial 类型的 queue,即 FIFO
_queue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL);
_maxLocalEventCount = 1000;
// 初始化事件数据,从文件路径中读取数据
[self readAllEventsFromFilePath:_filePath];
}
return self;
}
第二步:在 - saveEvent: 方法插入数据之前,先判断已缓存的事件条数是否超过了本地可缓存的事件条数,如果已经超过,则删除最旧的事件
- (void)saveEvent:(NSDictionary *)event {
dispatch_async(self.queue, ^{
if (self.events.count >= _maxLocalEventCount) {
[self.events removeObjectAtIndex:0];
}
// 在数组中直接添加事件数据
[self.events addObject:event];
// 将事件数据保存在文件中
[self writeEventsToFile];
});
}
我们可以使用文件缓存实现事件数据的持久化操作。
首先,主要实现了一下三个功能:
然后有进行了两项优化
文件缓存相对来说还是比较简单,主要操作就是写文件和读取文件。每次写入的 数据量越大,文件缓存的性能越好。
当然,文件缓存是不够灵活的,我们很难使用更细的颗粒去操作数据。比如很难对某一条数据进行读写操作。
在 iOS 应用程序中,使用的数据库一般是 SQLite 数据库,SQLite 是轻量级数据库,数据存储简单高效,使用也非常简单,只是需要在项目中添加 libssqlite3.0 依赖,并在使用的时候引入 sqlite3.h 头文件即可。
第一步:创建 SensorsAnalyticsDatabase 工具类
//
// SensorsAnalyticsDatabase.h
// SensorsSDK
//
// Created by 任伟 on 2022/4/13.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SensorsAnalyticsDatabase : NSObject
/// 数据库文件的路径
@property (nonatomic, copy, readonly) NSString *filePath;
//+ (instancetype)new NS_UNAVAILABLE;
//- (instancetype)init NS_UNAVAILABLE;
/// 初始化方法
/// @param filePath 数据库路径,如果是nil, 使用默认路径
- (instancetype)initWithFilePath:(nullable NSString *)filePath NS_DESIGNATED_INITIALIZER;
/// 同步向数据库插入事件数据
/// @param event 事件
- (void)insertEvent: (NSDictionary *) event;
/// 从数据库中获取事件数据
/// @param count 获取事件数据条数
- (NSArray<NSString *> *)selectEventsForCount:(NSInteger)count;
/// 从数据库中删除一定数量的事件数据
/// @param count 需要删除的事件条数
- (BOOL)deleteEventsForCount:(NSInteger)count;
@end
NS_ASSUME_NONNULL_END
//
// SensorsAnalyticsDatabase.m
// SensorsSDK
//
// Created by 任伟 on 2022/4/13.
//
#import "SensorsAnalyticsDatabase.h"
#import <sqlite3.h>
static NSString * const SensorsAnalyticsDefaultDatabaseName = @"SensorsAnalyticsDatabase.sqlite";
@interface SensorsAnalyticsDatabase()
/// 数据库文件的路径
@property (nonatomic, copy) NSString *filePath;
/// 数据库私有属性
@property (nonatomic) sqlite3 *database;
/// 串行队列
@property (nonatomic, strong) dispatch_queue_t queue;
@end
@implementation SensorsAnalyticsDatabase {
sqlite3 *_database;
}
- (instancetype)init {
return [self initWithFilePath:nil];
}
- (instancetype)initWithFilePath:(NSString *)filePath {
self = [super init];
if (self) {
_filePath = filePath ?: [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultDatabaseName];
// 初始化队列的唯一标识
NSString *label = [NSString stringWithFormat:@"cn.sensorsdata.serialQueue.%p", self];
// 创建一个 serial 类型的 queue,即 FIFO
_queue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL);
// 打开数据库
[self open];
}
return self;
}
- (void)open {
dispatch_async(self.queue, ^{
// 初始化 SQLite 库
if (sqlite3_initialize() != SQLITE_OK) {
return;
}
// 打开数据库,获取数据库指针
if (sqlite3_open_v2([self.filePath UTF8String], &(self->_database), SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL) != SQLITE_OK) {
return NSLog(@"SQLite stmt prepare error: %s", sqlite3_errmsg(self.database));
}
char *error;
// 创建数据库表的 SQL 语句
// NSString *sql = @"CREATE TABLE IF NOT EXISTS events(id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL UNIQUE);";
NSString *sql = @"CREATE TABLE IF NOT EXISTS events (id integer PRIMARY KEY AUTOINCREMENT, event BLOB);";
// 运行创建表格的 SQL 语句
if (sqlite3_exec(self.database, [sql UTF8String], NULL, NULL, &error) != SQLITE_OK) {
return NSLog(@"Create events failure %s", error);
}
});
}
- (void)insertEvent:(NSDictionary *)event {
dispatch_async(self.queue, ^{
// 自定义 SQLite Statement
sqlite3_stmt *stmt;
// 插入语句
NSString *sql = @"INSERT INTO events (event) values (?)";
// 准备执行 SQL 语句,获取 sqlite3_stmt
if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &stmt, NULL) != SQLITE_OK) {
// 准备执行 SQL 语句失败,打印 log 返回失败 NO
return NSLog(@"SQLite stmt prepare error: %s", sqlite3_errmsg(self.database));
}
NSError *error;
// 将 event 转换成 JSON 数据
NSData *data = [NSJSONSerialization dataWithJSONObject:event options:NSJSONWritingPrettyPrinted error:&error];
if (error) {
return NSLog(@"The JSON object`s serialization error: %@", error);
}
// 将JSON数据与 stmt 绑定
sqlite3_bind_blob(stmt, 1, data.bytes, (int)data.length, SQLITE_TRANSIENT);
// 执行 stmt
if (sqlite3_step(stmt) != SQLITE_DONE) {
// 执行失败,打印log,返回失败(NO)
return NSLog(@"Insert event into events error");
}
});
}
- (NSArray<NSString *> *)selectEventsForCount:(NSInteger)count {
// 初始化数组,用于存储查询到的事件数据
NSMutableArray<NSString *> *events = [NSMutableArray arrayWithCapacity:count];
dispatch_sync(self.queue, ^{
// 自定义 SQLite Statement
sqlite3_stmt *stmt;
// 查询语句
NSString *sql = [NSString stringWithFormat:@"SELECT id, event FROM events ORDER BY id ASC LIMIT %lu", (unsigned long)count];
// 准备执行 SQL 语句,获取sqlite3——stmt
if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &stmt, NULL) != SQLITE_OK) {
// 准备执行 SQL 语句失败,打印log返回失败(no)
return NSLog(@"SQLite stmt prepare error: %s,", sqlite3_errmsg(self.database));
}
// 执行 SQL 语句
while (sqlite3_step(stmt) == SQLITE_ROW) {
// 将当前查询的这条数据转换成 NSData 对象
NSData *data = [[NSData alloc] initWithBytes:sqlite3_column_blob(stmt, 1) length:sqlite3_column_bytes(stmt, 1)];
// 将查询到的时间数据转换成JSON字符串
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
#ifdef DUBUG
NSLog(@"%@", jsonString);
#endif
// 将JSON字符串添加到数组中
[events addObject:jsonString];
}
});
return events;
}
- (BOOL)deleteEventsForCount:(NSInteger)count {
__block BOOL success = YES;
dispatch_sync(self.queue, ^{
// 删除语句
NSString *sql = [NSString stringWithFormat:@"DELETE FROM events WHERE id IN (SELECT id FROM events ORDER BY id ASC LIMIT %lu);", (unsigned long)count];
char *errmsg;
//执行删除语句
if (sqlite3_exec(self.database, sql.UTF8String, NULL, NULL, &errmsg) != SQLITE_OK) {
success = NO;
return NSLog(@"Failed to delete record msg=%s", errmsg);
}
});
return success;
}
@end
第二步:在 SensorsAnalyticsSDK.m 文件中新增 SensorsAnalyticsDatabase 类型私有属性 database,并在 -init 方法中进行初始化
- (instancetype)init {
self = [super init];
if (self) {
_automaticProperties = [self collectAutomaticProperties];
// 设置是否需是被动启动标记
_launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
_loginId = [[NSUserDefaults standardUserDefaults] objectForKey:SensorsAnalyticsLoginId];
_trackTimer = [NSMutableDictionary dictionary];
_enterBackgroundTrackTimerEvents = [NSMutableArray array];
_fileStroe = [[SensorsAnalyticsFileStore alloc] init];
_database = [[SensorsAnalyticsDatabase alloc] init];
// 添加应用程序状态监听
[self setupListeners];
}
return self;
}
第三步:修改 -track: properties: 的数据存储方式
- (void)track:(NSString *)eventName properties:(nullable NSDictionary<NSString *, id> *)properties {
NSMutableDictionary *event = [NSMutableDictionary dictionary];
// 设置事件 distinct_id 字段,用于唯一标识一个用户
event[@"distinct_id"] = self.loginId ?: self.anonymousId;
// 设置事件名称
event[@"event"] = eventName;
// 事件发生的时间戳,单位毫秒
event[@"time"] = [NSNumber numberWithLong:NSDate.date.timeIntervalSince1970 *1000];
NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
// 添加预置属性
[eventProperties addEntriesFromDictionary:self.automaticProperties];
// 添加自定义属性
[eventProperties addEntriesFromDictionary:properties];
// 判断是否是被动启动状态
if (self.isLaunchedPassively) {
eventProperties[@"$app_state"] = @"background";
}
// 设置事件属性
event[@"propeerties"] = eventProperties;
// 打印
[self printEvent:event];
// [self.fileStroe saveEvent:event];
[self.database insertEvent:event];
}
第四步:测试验证(和文件存储验证方式一样)
需要优化的内容:
在每次插入和查询数据的时候,都会执行 “准备执行SQL的语句”的操作,比较浪费资源
在查询和删除操作时,如果数据表中没有存储任何的数据,其实无须执行 SQL 语句
static sqlite3_stmt *insertStmt = NULL;
- (void)insertEvent:(NSDictionary *)event {
dispatch_async(self.queue, ^{
if (insertStmt) {
// 重置插入语句,重置之后可重新绑定数据
sqlite3_reset(insertStmt);
} else {
// 插入语句
NSString *sql = @"INSERT INTO events (event) values (?)";
// 准备执行 SQL 语句,获取 sqlite3_stmt
if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &insertStmt, NULL) != SQLITE_OK) {
// 准备执行 SQL 语句失败,打印 log 返回失败 NO
return NSLog(@"SQLite stmt prepare error: %s", sqlite3_errmsg(self.database));
}
}
NSError *error;
// 将 event 转换成 JSON 数据
NSData *data = [NSJSONSerialization dataWithJSONObject:event options:NSJSONWritingPrettyPrinted error:&error];
if (error) {
return NSLog(@"The JSON object`s serialization error: %@", error);
}
// 将JSON数据与 insertStmt 绑定
sqlite3_bind_blob(insertStmt, 1, data.bytes, (int)data.length, SQLITE_TRANSIENT);
// 执行 stmt
if (sqlite3_step(insertStmt) != SQLITE_DONE) {
// 执行失败,打印log,返回失败(NO)
return NSLog(@"Insert event into events error");
}
});
}
// 最后一次查询下的事件数量
static NSUInteger lastSelectEventCount = 50;
static sqlite3_stmt *selectStmt = NULL;
- (NSArray<NSString *> *)selectEventsForCount:(NSInteger)count {
// 初始化数组,用于存储查询到的事件数据
NSMutableArray<NSString *> *events = [NSMutableArray arrayWithCapacity:count];
dispatch_sync(self.queue, ^{
if (count != lastSelectEventCount) {
lastSelectEventCount = count;
selectStmt = NULL;
}
if (selectStmt) {
// 重置插入语句,重置之后可重新查询数据
sqlite3_reset(selectStmt);
} else {
// 查询语句
NSString *sql = [NSString stringWithFormat:@"SELECT id, event FROM events ORDER BY id ASC LIMIT %lu", (unsigned long)count];
// 准备执行 SQL 语句,获取sqlite3——stmt
if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &selectStmt, NULL) != SQLITE_OK) {
// 准备执行 SQL 语句失败,打印log返回失败(no)
return NSLog(@"SQLite stmt prepare error: %s,", sqlite3_errmsg(self.database));
}
}
// 执行 SQL 语句
while (sqlite3_step(selectStmt) == SQLITE_ROW) {
// 将当前查询的这条数据转换成 NSData 对象
NSData *data = [[NSData alloc] initWithBytes:sqlite3_column_blob(selectStmt, 1) length:sqlite3_column_bytes(stmt, 1)];
// 将查询到的时间数据转换成JSON字符串
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
#ifdef DUBUG
NSLog(@"%@", jsonString);
#endif
// 将JSON字符串添加到数组中
[events addObject:jsonString];
}
});
return events;
}
添加一个方法用于查询数据库已经存储事件条数,新增一个 eventCount 属性,初始化时,他的数值就是当前数据库已经存储事件条数,每次成功插入一条数据的时候值对应的加1,在删除数据的时候减去相应删除的数据条数,这样就保证 eventCount 和本地数据存储的事件条数一致,减少查询次数。
第一步:在 SensorsAnalyticsDatabase.h 中新增 eventCount 属性
/// 本地事件存储总量
@property (nonatomic) NSUInteger eventCount;
第二步:在 SensorsAnalyticsDatabase.m 文件中新增私有方法 - queryLocalDatabaseEventCount,查询数据库中已经缓存事件数。
// 查询数据库中已经缓存事件的条数
- (void)queryLocalDatabaseEventCount {
dispatch_async(self.queue, ^{
// 查询语句
NSString *sql = @"SELECT count(*) FORM events";
sqlite3_stmt *stmt = NULL;
// 准备执行SQL语句,获取 sqlite3_stmt
if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &stmt, NULL) != SQLITE_OK) {
// 准备执行SQL语句失败,打印log返回失败 NO
return NSLog(@"SQLite stmt prepare error: %s", sqlite3_errmsg(self.database));
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
self.eventCount = sqlite3_column_int(stmt, 0);
}
});
}
第三步 :在 - initWithFilePath: 初始化方法中调用 - queryLocalDatabaseEventCount,初始化 eventCount
- (instancetype)initWithFilePath:(NSString *)filePath {
self = [super init];
if (self) {
_filePath = filePath ?: [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:SensorsAnalyticsDefaultDatabaseName];
// 初始化队列的唯一标识
NSString *label = [NSString stringWithFormat:@"cn.sensorsdata.serialQueue.%p", self];
// 创建一个 serial 类型的 queue,即 FIFO
_queue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL);
// 打开数据库
[self open];
[self queryLocalDatabaseEventCount];
}
return self;
}
第四步:优化 - insertEvent: 方法,事件插入成功,事件数量 eventCount 加 1
static sqlite3_stmt *insertStmt = NULL;
- (void)insertEvent:(NSDictionary *)event {
dispatch_async(self.queue, ^{
if (insertStmt) {
// 重置插入语句,重置之后可重新绑定数据
sqlite3_reset(insertStmt);
} else {
// 插入语句
NSString *sql = @"INSERT INTO events (event) values (?)";
// 准备执行 SQL 语句,获取 sqlite3_stmt
if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &insertStmt, NULL) != SQLITE_OK) {
// 准备执行 SQL 语句失败,打印 log 返回失败 NO
return NSLog(@"SQLite stmt prepare error: %s", sqlite3_errmsg(self.database));
}
}
NSError *error;
// 将 event 转换成 JSON 数据
NSData *data = [NSJSONSerialization dataWithJSONObject:event options:NSJSONWritingPrettyPrinted error:&error];
if (error) {
return NSLog(@"The JSON object`s serialization error: %@", error);
}
// 将JSON数据与 insertStmt 绑定
sqlite3_bind_blob(insertStmt, 1, data.bytes, (int)data.length, SQLITE_TRANSIENT);
// 执行 stmt
if (sqlite3_step(insertStmt) != SQLITE_DONE) {
// 执行失败,打印log,返回失败(NO)
return NSLog(@"Insert event into events error");
}
// 数据插入成功 事件数量加1
self.eventCount ++;
});
}
第五步:优化 - deleteEventsForCount: 方法,当 eventCount 为 0 时,直接返回;当数据删除成功时,事件数量减去相应的删除条数
- (BOOL)deleteEventsForCount:(NSInteger)count {
__block BOOL success = YES;
dispatch_sync(self.queue, ^{
// 当本地事件数量为 0 时,直接返回
if (self.eventCount == 0) {
return;
}
// 删除语句
NSString *sql = [NSString stringWithFormat:@"DELETE FROM events WHERE id IN (SELECT id FROM events ORDER BY id ASC LIMIT %lu);", (unsigned long)count];
char *errmsg;
//执行删除语句
if (sqlite3_exec(self.database, sql.UTF8String, NULL, NULL, &errmsg) != SQLITE_OK) {
success = NO;
return NSLog(@"Failed to delete record msg=%s", errmsg);
}
self.eventCount = self.eventCount < count ? 0 : self.eventCount - count;
});
return success;
}
第六步:优化 - selectEventsForCount: 方法,当 eventCount 为 0 时,直接返回
// 最后一次查询下的事件数量
static NSUInteger lastSelectEventCount = 50;
static sqlite3_stmt *selectStmt = NULL;
- (NSArray<NSString *> *)selectEventsForCount:(NSInteger)count {
// 初始化数组,用于存储查询到的事件数据
NSMutableArray<NSString *> *events = [NSMutableArray arrayWithCapacity:count];
dispatch_sync(self.queue, ^{
// 当本地事件数量为 0 ,直接返回
if (self.eventCount == 0) {
return;
}
if (count != lastSelectEventCount) {
lastSelectEventCount = count;
selectStmt = NULL;
}
if (selectStmt) {
// 重置插入语句,重置之后可重新查询数据
sqlite3_reset(selectStmt);
} else {
// 查询语句
NSString *sql = [NSString stringWithFormat:@"SELECT id, event FROM events ORDER BY id ASC LIMIT %lu", (unsigned long)count];
// 准备执行 SQL 语句,获取sqlite3——stmt
if (sqlite3_prepare_v2(self.database, sql.UTF8String, -1, &selectStmt, NULL) != SQLITE_OK) {
// 准备执行 SQL 语句失败,打印log返回失败(no)
return NSLog(@"SQLite stmt prepare error: %s,", sqlite3_errmsg(self.database));
}
}
// 执行 SQL 语句
while (sqlite3_step(selectStmt) == SQLITE_ROW) {
// 将当前查询的这条数据转换成 NSData 对象
NSData *data = [[NSData alloc] initWithBytes:sqlite3_column_blob(selectStmt, 1) length:sqlite3_column_bytes(stmt, 1)];
// 将查询到的时间数据转换成JSON字符串
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
#ifdef DUBUG
NSLog(@"%@", jsonString);
#endif
// 将JSON字符串添加到数组中
[events addObject:jsonString];
}
});
return events;
}
通过上面我们实现了数据库缓存事件数据,并实现了如下功能
然后对数据缓存性能进行了优化。对于文件缓存来说,数据库缓存更加灵活,可以实现对单条数据的查询、插入和删除操作,同时调试也更容易。SQLite 数据库也有极高的性能,特别是对单条数据的操作,性能明显由于文件缓存。
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',
这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的
我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
我正在编写一个简单的静态Rack应用程序。查看下面的config.ru代码:useRack::Static,:urls=>["/elements","/img","/pages","/users","/css","/js"],:root=>"archive"map'/'dorunProc.new{|env|[200,{'Content-Type'=>'text/html','Cache-Control'=>'public,max-age=6400'},File.open('archive/splash.html',File::RDONLY)]}endmap'/pages/search.
相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声
首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有, 也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加