iOS全埋點解決方案-數據存儲

来源:https://www.cnblogs.com/r360/archive/2022/05/23/16299922.html
-Advertisement-
Play Games

前言 ​ SDK 需要把事件數據緩衝到本地,待符合一定策略再去同步數據。 一、數據存儲策略 ​ 在 iOS 應用程式中,從 “數據緩衝在哪裡” 這個緯度看,緩衝一般分兩種類型。 記憶體緩衝 磁碟緩衝 ​ 記憶體緩衝是將數據緩衝在記憶體中,供應用程式直接讀取和使用。優點是讀取速度快。缺點是由於記憶體資源有限, ...


前言

​ 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];
}

第五步:測試驗證image-20220413142424772

第六步:在文件中讀取和刪除事件數據

@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 資料庫也有極高的性能,特別是對單條數據的操作,性能明顯由於文件緩存。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、概述 DataX 是阿裡雲 DataWorks數據集成 的開源版本,在阿裡巴巴集團內被廣泛使用的離線數據同步工具/平臺。DataX 實現了包括 MySQL、Oracle、OceanBase、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS) ...
  • 1、查詢用戶所擁有的安全形色: select su.FullName,r.Name,bu.name bussinessname from SystemUserRoles sur left join SystemUserBase su on su.SystemUserId = sur.SystemUs ...
  • **導讀:**數據安全立法2018年9月於十三屆全國人大常委會列入立法規劃。經過三次審議,在2021年6月10日,十三屆全國人大常委會第二十九次會議正式表決通過,並於2021年9月1日起施行。從法律角度來說,國家對於數據安全越來越重視,作為企業該如何針對數據安全法進行數據安全治理的規劃,最終進行對應 ...
  • 本期我們將帶來“分散式計算器”的開發,幫助大家瞭解聲明式開發範式的UI描述、組件化機制、UI狀態管理、渲染控制語法等核心機制和功能。 ...
  • 本期,我們通過介紹 OpenHarmony 的硬體資源池化框架,為大家揭曉 OpenHarmony 是如何實現多設備協同的。 ...
  • 原文地址:Android shape與selector標簽使用 Android中提供一種xml的方式,讓我們可以自由地定義背景,比較常用的就是shape標簽和selector標簽 shape shape的翻譯為形狀的意思,一般用來定義背景的形狀,如長方形,線條,圓形 rectangle 矩形 預設 ...
  • **版本:**Android 9 **平臺:**RK-PX30 問題描述:在狀態欄點擊藍牙圖標打開藍牙時,發現打開失敗,進入到設置里打開藍牙卻能打開成功,但是時間會比較長。 分析: 查看日誌發現,執行BluetoothHci::initialize()後,在獲取到藍牙MAC地址,就又把藍牙關閉了。測 ...
  • 【導讀】 AR技術,是一種將真實世界信息和虛擬世界信息“無縫”銜接的技術,現如今AR技術受到日益廣泛的關註,在我們生活中發揮著重要的作用,並顯示出巨大的潛力……它是如何改變我們觀察世界的方式?本次直播,讓我們一起探索HMS Core AR Engine是如何幫助開發者們構築立體世界,打造沉浸式營銷的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...