前言
SDK 需要把事件資料緩衝到本地,待符合一定策略再去同步資料。
一、資料儲存策略
在 iOS 應用程式中,從 “資料緩衝在哪裡” 這個緯度看,緩衝一般分兩種型別。
- 記憶體緩衝
- 磁碟緩衝
記憶體緩衝是將資料緩衝在記憶體中,供應用程式直接讀取和使用。優點是讀取速度快。缺點是由於記憶體資源有限,應用程式在系統中申請的記憶體,會隨著應用生命週期結束而被釋放,會導致記憶體中的資料丟失,因此將事件資料緩衝到記憶體中不是最佳選擇。
磁碟緩衝是將資料緩衝到磁碟空間中,其特點正好和磁碟緩衝相反。磁碟緩衝容量打,但是讀寫速度對於記憶體緩衝要慢點。不過磁碟緩衝可以持久化儲存,不受應用程式生命週期影響。因為,將資料儲存在磁碟中,丟失的風險比較低。即使磁碟緩衝資料速度較慢,但綜合考慮,磁碟緩衝是緩衝事件資料最優的選擇。
1.1 沙盒
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
沙盒的根目錄下有三個常用的資料夾:
- Document
- Library
- tmp
(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
- Preferences
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 資料夾中。
1.2 資料快取
在 iOS 應用程式中,一般通過兩種方式進行磁碟快取:
- 檔案快取
- 資料庫快取
這兩種方式都是可以實現資料採集 SDK 的緩衝機制。緩衝的策略即當事件發生後,先將事件資料儲存在快取中,待符合一定策略後從快取中讀取事件資料並進行同步,同步成功後,將已同步的事件從快取中刪除。
對於寫入的效能,SQLite 資料庫優於檔案快取.
對於讀取的效能:如果單條資料小於 100KB,則 SQLite 資料庫讀取的速度更快。如果單條資料大於 100KB,則從檔案中讀取的速度更快。
因此,資料採集 SDK 一般都是使用 SQLite 資料庫來快取資料,這樣可以擁有最佳的讀寫效能。如果希望採集更完整,更全面的資訊,比如採集使用者操作時當前截圖的資訊(一般超過100KB),檔案快取可能是最優的選擇。
二、檔案快取
可以使用 NSKeyedArchiver 類將字典物件進行歸檔並寫入檔案,也可以使用 NSJSONSerialization 類把字典物件轉成 JSON 格式字串寫入檔案。
2.1 實現步驟
第一步:新建處理檔案的工具類 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];
}
2.2 優化
通過上面實現檔案快取存在兩個非常明細的問題。
(1)如果在主執行緒中觸發事件,那麼讀取事件、儲存事件及刪除事件都在主執行緒中執行,會出現所謂的 “卡主執行緒”問題。
(2)在無網環境下,如果在檔案中快取了大量的事件資料,會導致記憶體佔用過大,影響應用程式效能。
2.2.1 多執行緒優化
解決 “卡主執行緒” 問題的方法主要是把處理檔案的邏輯都放到多執行緒中執行。
第一步:在 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
2.2.2 記憶體優化
設定一個本地可快取的最大事件條數,當本地已經快取到事件條數超過本地可快取最大事件條數時,刪除最舊的事件資料。以保證最新的事件資料可以被快取。
第一步:在 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];
});
}
2.3 總結
我們可以使用檔案快取實現事件資料的持久化操作。
首先,主要實現了一下三個功能:
- 儲存事件
- 獲取本地快取的所有事件
- 刪除事件
然後有進行了兩項優化
- 多執行緒優化
- 記憶體優化
檔案快取相對來說還是比較簡單,主要操作就是寫檔案和讀取檔案。每次寫入的 資料量越大,檔案快取的效能越好。
當然,檔案快取是不夠靈活的,我們很難使用更細的顆粒去運算元據。比如很難對某一條資料進行讀寫操作。
三、資料庫快取
在 iOS 應用程式中,使用的資料庫一般是 SQLite 資料庫,SQLite 是輕量級資料庫,資料儲存簡單高效,使用也非常簡單,只是需要在專案中新增 libssqlite3.0 依賴,並在使用的時候引入 sqlite3.h 標頭檔案即可。
3.1 實現步驟
第一步:建立 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];
}
第四步:測試驗證(和檔案儲存驗證方式一樣)
3.2 優化
需要優化的內容:
在每次插入和查詢資料的時候,都會執行 “準備執行SQL的語句”的操作,比較浪費資源
在查詢和刪除操作時,如果資料表中沒有儲存任何的資料,其實無須執行 SQL 語句
(1)快取 sqlite3_stmt
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;
}
(2)快取事件總條數
新增一個方法用於查詢資料庫已經儲存事件條數,新增一個 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;
}
3.3 總結
通過上面我們實現了資料庫快取事件資料,並實現瞭如下功能
- 插入資料
- 查詢資料
- 刪除資料
然後對資料快取效能進行了優化。對於檔案快取來說,資料庫快取更加靈活,可以實現對單條資料的查詢、插入和刪除操作,同時除錯也更容易。SQLite 資料庫也有極高的效能,特別是對單條資料的操作,效能明顯由於檔案快取。