iOS全埋點解決方案-數據同步

来源:https://www.cnblogs.com/r360/archive/2022/06/06/16347174.html
-Advertisement-
Play Games

前言 ​ 將本地存儲的事件數據同步到伺服器,然後經過服務端的存儲、抽取、分析和展示,充分發揮數據真正的價值。 一、數據同步 第一步:在 SensorsSDK 項目中,新增 SensorsAnalyticsNetwork 工具類,並新增 serverURL 用於保存伺服器 URL 地址 #import ...


image-20220421103455429

前言

​ 將本地存儲的事件數據同步到伺服器,然後經過服務端的存儲、抽取、分析和展示,充分發揮數據真正的價值。

一、數據同步

第一步:在 SensorsSDK 項目中,新增 SensorsAnalyticsNetwork 工具類,並新增 serverURL 用於保存伺服器 URL 地址

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SensorsAnalyticsNetwork : NSObject

/// 數據上報的伺服器
@property (nonatomic, strong) NSURL *serverURL;

@end

NS_ASSUME_NONNULL_END

第二步:新增 - initWithServerURL: 初始化方法,並禁用 - init 初始化方法

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SensorsAnalyticsNetwork : NSObject

/// 數據上報的伺服器
@property (nonatomic, strong) NSURL *serverURL;

/// 禁止使用 - init 方法進行初始化
- (instancetype)init NS_UNAVAILABLE;

/// 指定初始化方法
/// @param serverURL 伺服器 URL 地址
- (instancetype)initWithServerURL:(NSURL *)serverURL NS_DESIGNATED_INITIALIZER;

@end

NS_ASSUME_NONNULL_END

第三步:在 SensorsAnalyticsNetwork.m 中新增 NSURLSession 類型的 session 屬性,併在 - initWithServerURL:方法中進行初始化

@interface SensorsAnalyticsNetwork() <NSURLSessionDelegate>

@property (nonatomic, strong) NSURLSession *session;

@end

@implementation SensorsAnalyticsNetwork

- (instancetype)initWithServerURL:(NSURL *)serverURL {
    self = [super init];
    if (self) {
        _serverURL = serverURL;
        
        // 創建預設的 session 配置對象
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
        // 設置當個逐級連接數為 5
        configuration.HTTPMaximumConnectionsPerHost = 5;
        // 設置請求的超時事件
        configuration.timeoutIntervalForRequest = 30;
        // 容許使用蜂窩網路連接
        configuration.allowsCellularAccess = YES;
        
        // 創建一個網路請求回調和完成操作的線程池
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        // 設置同步運行的最大操作數為 1, 即個操作FIFO
        queue.maxConcurrentOperationCount = 1;
        // 通過配置對象創建一個 session 對象
        _session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:queue];
    }
    
    return self;
}

第四步:新增 - buildJSONStringWithEvents:方法,將事件數字轉出字元串

/// 將事件數組轉換成字元串
- (NSString *)buildJSONStringWithEvents:(NSArray<NSString *> *)events {
    return [NSString stringWithFormat:@"[\n%@\n]", [events componentsJoinedByString:@".\n"]];
}

第五步:新增 - buildRequestWithJSONString:方法,用於根據 serverURL 和事件字元串來創建 NSURLRequest 請求

- (NSURLRequest *)buildRequestWithJSONString:(NSString *)json {
    // 通過伺服器 URL 地址創建請求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.serverURL];
    // 設置請求的body
    request.HTTPBody = [json dataUsingEncoding:NSUTF8StringEncoding];
    // 請求方法
    request.HTTPMethod = @"POST";
    
    return request;
}

第六步:新增 - flushEvents: 方法,用於同步數據

/// 網路請求結束處理回調類型
typedef void(^SAURLSessionTaskCompletionHandler) (NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error);

- (BOOL)flushEvents:(NSArray<NSString *> *)events {
    // 將事件數組組裝成JSON字元串
    NSString *jsonString = [self buildJSONStringWithEvents:events];
    // 創建請求對象
    NSURLRequest *request = [self buildRequestWithJSONString:jsonString];
    // 數據上傳結果
    __block BOOL flushSuccess = NO;
    // 使用 GCD 中信號量,實現線程鎖
    dispatch_semaphore_t flushSemaphore = dispatch_semaphore_create(0);
    SAURLSessionTaskCompletionHandler handler = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            // 當前請求發送錯誤,列印信息錯誤
            NSLog(@"Flush events error: %@", error);
            dispatch_semaphore_signal(flushSemaphore);
            return;
        }
        
        // 獲取請求結束返回的狀態碼
        NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
        // 當狀態碼為 2xx 時,表示事件發送成功
        if (statusCode >= 200 && statusCode < 300) {
            // 列印上傳成功的數據
            NSLog(@"Flush events success: %@", jsonString);
            // 數據上報成功
            flushSuccess = YES;
        } else {
            // 事件信息發送失敗
            NSString *desc = [NSString stringWithFormat:@"Flush events error, statusCode: %d, events: %@", (int)statusCode, jsonString];
            NSLog(@"Flush events error: %@", desc);
        }
        dispatch_semaphore_signal(flushSemaphore);
    };
    
    // 通過 request 創建請求任務
    NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:handler];
    // 執行任務
    [task resume];
    // 等待請求完成
    dispatch_semaphore_wait(flushSemaphore, DISPATCH_TIME_FOREVER);
    // 返回數據上傳結果
    return flushSuccess;
}

第七步:在 SensorsAnalyticsSDK.m 文件中新增 SensorsAnalyticsNetwork 類型的 network 對象,併在 - init 方法中進行初始化

#import "SensorsAnalyticsNetwork.h"

/// 發送網路請求對象
@property (nonatomic, strong) SensorsAnalyticsNetwork *network;

- (instancetype)initWithServerURL:(NSString *)urlString {
    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];
        
        _network = [[SensorsAnalyticsNetwork alloc] initWithServerURL:[NSURL URLWithString:urlString]];
        
        // 添加應用程式狀態監聽
        [self setupListeners];
    }
    return self;
}

第八步:暴露 - flush 數據上報的方法,並實現。

/// 向伺服器同步本地所有數據
- (void)flush;
- (void)flush {
    // 預設向服務端一次發送 50 條數據
    [self flushByEventCount:SensorsAnalyticsDefalutFlushEventCount];
}

- (void)flushByEventCount:(NSUInteger) count {
    // 獲取本地數據
    NSArray<NSString *> *events = [self.database selectEventsForCount:count];
    // 當本地存儲的數據為0或者上傳 失敗時,直接返回,退出遞歸調用
    if (events.count == 0 || ![self.network flushEvents:events]) {
        return;
    }
    // 當刪除數據失敗時,直接返回,退出遞歸調用,防止死迴圈
    if ([self.database deleteEventsForCount:count]) {
        return;
    }
    // 繼續刪除本地的其他數據
    [self flushByEventCount:count];
}

第九步:測試驗證

問題描述:serverURL 是在 -init 方法中硬編碼的。我們需要支持 SDK 初始化的時候傳入

第一步:在 SensorsAnalyticsSDK.h 文件中禁止直接使用 - init 方法初始化 SDK,並新增 + startWithServerURL:方法的聲明

@interface SensorsAnalyticsSDK : NSObject

/// 設備 ID (匿名 ID)
@property (nonatomic, copy) NSString *anonymousId;

/// 事件開始發生的時間戳
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSDictionary *> *trackTimer;

/// 獲取 SDK 實例方法
/// 返回單例對象
+ (SensorsAnalyticsSDK *)sharedInstance;


/// 用戶登陸設置登陸 ID
/// @param loginId 用戶的登陸 ID
- (void)login:(NSString *)loginId;


/// 當前的時間
+ (double)currentTime;

/// 系統啟動時間
+ (double)systemUpTime;

/// 向伺服器同步本地所有數據
- (void)flush;

- (instancetype)init NS_UNAVAILABLE;

/// 初始化 SDK
/// @param urlString 接受數據的服務端URL
+ (void)startWithServerURL:(NSString *) urlString;

@end

第二步:實現 - initWithServerURL:方法的初始化

- (instancetype)initWithServerURL:(NSString *)urlString {
    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];
        
        _network = [[SensorsAnalyticsNetwork alloc] initWithServerURL:[NSURL URLWithString:urlString]];
        
        // 添加應用程式狀態監聽
        [self setupListeners];
    }
    return self;
}

第三步:實現 + startWithServerURL: 類方法

static SensorsAnalyticsSDK *sharedInstace = nil;
+ (void)startWithServerURL:(NSString *)urlString {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstace = [[SensorsAnalyticsSDK alloc] initWithServerURL:urlString];
    });
}

第四步: 修改 + sharedInstance 方法

+ (SensorsAnalyticsSDK *)sharedInstance {
    return sharedInstace;
}

第五步:測試驗證

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    [SensorsAnalyticsSDK startWithServerURL:@"https//:www.baidu.com"];
    [[SensorsAnalyticsSDK sharedInstance] track:@"MyFirstTrack" properties:@{@"testKey": @"testValue"}];
    
    return YES;
}

二、數據同步策略

​ 上面的集成,需要手動觸發。但是作為一個標準的數據採集 SDK,必須包含一些自動同步數據的策略,一方面是為了降低用戶使用 SDK 的難度和成本,另一方面是為了確保數據的正確性、完整性和及時性。

2.1 基本原則

  • 策略一:客戶端本地已經緩存的事件超過一定條數時同步數據
  • 策略二:客戶端每隔一定的時間不同一次數據
  • 策略三:應用程式進入後天時嘗試不同本地已緩存的所有數據

​ 因為事件和事件之間是有先後順序的,因此,在同步數據的時候,需要嚴格按照事件觸發的時間吸納後順序同步數據。所以我們需要先優化 SDK 中 - flush 方法,並使其在隊列中執行。

第一步:在 SensorsAnalyticsSDK.m 中新增 dispatch_queue_t 類型的屬性 serialQueue,併在 -initWithServerURL:方法中進行初始化

/// 隊列
@property (nonatomic, strong) dispatch_queue_t serialQueue;

- (instancetype)initWithServerURL:(NSString *)urlString {
    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];
        
        _network = [[SensorsAnalyticsNetwork alloc] initWithServerURL:[NSURL URLWithString:urlString]];
        
        NSString *queueLabel = [NSString stringWithFormat:@"cn.sensorsdata.%@.%p", self.class, self];
        _serialQueue = dispatch_queue_create(queueLabel.UTF8String, DISPATCH_QUEUE_SERIAL);
        
        // 添加應用程式狀態監聽
        [self setupListeners];
    }
    return self;
}

第二步:修改 -flush 方法,並使其在隊列中執行

- (void)flush {
    dispatch_async(self.serialQueue, ^{
        // 預設向服務端一次發送 50 條數據
        [self flushByEventCount:SensorsAnalyticsDefalutFlushEventCount];
    });
}

第三步:優化 - 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;
    
    dispatch_async(self.serialQueue, ^{
        // 列印
        [self printEvent:event];
    //    [self.fileStroe saveEvent:event];
        [self.database insertEvent:event];
    });
}

2.2 策略一

​ 客戶端本地已經緩存的事件超過一定條數時同步數據。

​ 實現策略:每次事件觸發併入庫後,檢查一下已緩存的事件條數是否超過了定義的閾值,如果已達到,調用 - flush 方法同步數據。

第一步:添加屬性 flushBulkSize ,表示允許本地緩存的事件最大條數

/// 當本地緩存的事件達到最大條數時,上次數據(預設 100 條)
@property (nonatomic, assign) NSInteger flushBulkSize;

第二步:在 - initWithServerURL: 初始化中,初始化化 flushBulkSize 屬性,預設值設置為 100 條

- (instancetype)initWithServerURL:(NSString *)urlString {
    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];
        
        _network = [[SensorsAnalyticsNetwork alloc] initWithServerURL:[NSURL URLWithString:urlString]];
        
        NSString *queueLabel = [NSString stringWithFormat:@"cn.sensorsdata.%@.%p", self.class, self];
        _serialQueue = dispatch_queue_create(queueLabel.UTF8String, DISPATCH_QUEUE_SERIAL);
        
        _flushBulkSize = 100;
        
        // 添加應用程式狀態監聽
        [self setupListeners];
    }
    return self;
}

第三步:在 - track: properties: 方法,事件入庫之後,判斷本地緩存的事件條數是否大於 flushBulkSize ,入股大於,則觸發數據同步

- (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;
    
    dispatch_async(self.serialQueue, ^{
        // 列印
        [self printEvent:event];
    //    [self.fileStroe saveEvent:event];
        [self.database insertEvent:event];
    });
    
    if (self.database.eventCount >= self.flushBulkSize) {
        [self flush];
    }
}

2.3 策略二

​ 客戶端每隔一定的時間同步一次數據,(比如預設 15 秒)

​ 實現策略:開啟一個定時器,每隔一定時間調用一次 -flush 方法。

第一步:添加 flushInterval 屬性,兩次數據發送的時間間隔,然後再 - initWithServerURL: 初始化方法中初始化

/// 兩次數據發送的時間間隔,單位為秒
@property (nonatomic) NSUInteger flushInterval;

- (instancetype)initWithServerURL:(NSString *)urlString {
    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];
        
        _network = [[SensorsAnalyticsNetwork alloc] initWithServerURL:[NSURL URLWithString:urlString]];
        
        NSString *queueLabel = [NSString stringWithFormat:@"cn.sensorsdata.%@.%p", self.class, self];
        _serialQueue = dispatch_queue_create(queueLabel.UTF8String, DISPATCH_QUEUE_SERIAL);
        
        _flushBulkSize = 100;
        
        _flushInterval = 15;
        
        // 添加應用程式狀態監聽
        [self setupListeners];
        
        [self startFlushTimer];
    }
    return self;
}

第二步:新增 flushTimer 屬性,並實現 定時器方法

/// 定時上次事件的定時器
@property (nonatomic, strong) NSTimer *flushTimer;
#pragma mark - FlushTimer
- (void)startFlushTimer {
    if (self.flushTimer) {
        return;
    }
    NSTimeInterval interval = self.flushInterval < 5 ? 5 : self.flushInterval;
    self.flushTimer = [NSTimer timerWithTimeInterval:interval target:self selector:@selector(flush) userInfo:nil repeats:YES];
    [NSRunLoop.currentRunLoop addTimer:self.flushTimer forMode:NSRunLoopCommonModes];
}

- (void)stopFlushTimer {
    [self.flushTimer invalidate];
    self.flushTimer = nil;
}

第三步:在 - initWithServerURL: 中調用開啟定時器方法 - startFlushTimer

- (instancetype)initWithServerURL:(NSString *)urlString {
    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];
        
        _network = [[SensorsAnalyticsNetwork alloc] initWithServerURL:[NSURL URLWithString:urlString]];
        
        NSString *queueLabel = [NSString stringWithFormat:@"cn.sensorsdata.%@.%p", self.class, self];
        _serialQueue = dispatch_queue_create(queueLabel.UTF8String, DISPATCH_QUEUE_SERIAL);
        
        _flushBulkSize = 100;
        
        _flushInterval = 15;
        
        // 添加應用程式狀態監聽
        [self setupListeners];
        
        [self startFlushTimer];
    }
    return self;
}

第四步:實現 - setFlushInterval: 方法

- (void)setFlushInterval:(NSUInteger)flushInterval {
    if (_flushInterval != flushInterval) {
        _flushInterval = flushInterval;
        // 上傳本地緩存所有數據
        [self flush];
        // 先暫停定時器
        [self stopFlushTimer];
        // 重新開始定時器
        [self startFlushTimer];
    }
}

第五步:在 - applicationDidEnterBackground: 方法中停止定時器,在 - applicationDidBecomeActive: 中開啟定時器

- (void)applicationDidEnterBackground:(NSNotification *)notification {
    NSLog(@"Application did enter background.");
    
    // 還原標記位
    self.applicationWillResignActive = NO;
    
    // 觸發 AppEnd 事件
    // [self track:@"$AppEnd" properties:nil];
    [self trackTimerEnd:@"$AppEnd" properties:nil];
    
    // 暫停所有事件時長統計
    [self.trackTimer enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSDictionary * _Nonnull obj, BOOL * _Nonnull stop) {
        if (![obj[@"is_pause"] boolValue]) {
            [self.enterBackgroundTrackTimerEvents addObject:key];
            [self trackTimerPause:key];
        }
    }];
    
    // 停止計時器
    [self stopFlushTimer];
}

- (void)applicationDidBecomeActive:(NSNotification *)notification {
    NSLog(@"Application did enter active.");
    
    // 還原標記位
    if (self.applicationWillResignActive) {
        self.applicationWillResignActive = NO;
        return;
    }
    
    // 將被動啟動標記位設置為 NO,正常記錄事件
    self.launchedPassively = NO;
    
    // 觸發 AppStart 事件
    [self track:@"$AppStart" properties:nil];
    
    // 恢復所有的事件時長統計
    for (NSString *event in self.enterBackgroundTrackTimerEvents) {
        [self trackTimerStart:event];
    }
    [self.enterBackgroundTrackTimerEvents removeAllObjects];
    
    // 開始 $AppEnd 事件計時
    [self trackTimerStart:@"$AppEnd"];
    
    // 開啟定時器
    [self startFlushTimer];
}

2.4 策略三

​ 應用程式進入後天時嘗試不同本地已緩存的所有數據

​ 實現策略:通過 - beginBackgroundTaskWithExpirationHandler 方法,該方法可以讓我們在應用程式進入後臺最多有3分鐘的時間來處理數據。

第一步:新增 - flushByEventCount: 方法 新增 background 參數,表示是否後臺任務發起同步數據

- (void)flushByEventCount:(NSUInteger) count background:(BOOL)background{
    if (background) {
        __block BOOL isContinue = YES;
        dispatch_sync(dispatch_get_main_queue(), ^{
            // 當運行時間大於請求超時時間時,為保證資料庫刪除時應用程式不被強殺,不在繼續上傳
            isContinue = UIApplication.sharedApplication.backgroundTimeRemaining >= 30;
        });
        if (!isContinue) {
            return;
        }
    }
    // 獲取本地數據
    NSArray<NSString *> *events = [self.database selectEventsForCount:count];
    // 當本地存儲的數據為0或者上傳 失敗時,直接返回,退出遞歸調用
    if (events.count == 0 || ![self.network flushEvents:events]) {
        return;
    }
    // 當刪除數據失敗時,直接返回,退出遞歸調用,防止死迴圈
    if ([self.database deleteEventsForCount:count]) {
        return;
    }
    // 繼續刪除本地的其他數據
    [self flushByEventCount:count background:background];
}

第二步:- flush 調用修改後的方法

- (void)flush {
    dispatch_async(self.serialQueue, ^{
        // 預設向服務端一次發送 50 條數據
        [self flushByEventCount:SensorsAnalyticsDefalutFlushEventCount background:NO];
    });
}

第三步:- applicationDidEnterBackground: 添加後臺同步數據的任務

- (void)applicationDidEnterBackground:(NSNotification *)notification {
    NSLog(@"Application did enter background.");
        
    // 還原標記位
    self.applicationWillResignActive = NO;
    
    // 觸發 AppEnd 事件
    // [self track:@"$AppEnd" properties:nil];
    [self trackTimerEnd:@"$AppEnd" properties:nil];
    
    UIApplication *application = UIApplication.sharedApplication;
    // 初始化標記位
    __block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid;
    // 結束後臺任務
    void (^endBackgroundTast)(void) = ^() {
        [application endBackgroundTask:backgroundTaskIdentifier];
        backgroundTaskIdentifier = UIBackgroundTaskInvalid;
    };
    
    // 標記長時間運行的後臺任務
    backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
        endBackgroundTast();
    }];
    
    dispatch_async(self.serialQueue, ^{
        // 發送數據
        [self flushByEventCount:SensorsAnalyticsDefalutFlushEventCount background:YES];
        // 結束後臺任務
        endBackgroundTast();
    });
    
    // 暫停所有事件時長統計
    [self.trackTimer enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSDictionary * _Nonnull obj, BOOL * _Nonnull stop) {
        if (![obj[@"is_pause"] boolValue]) {
            [self.enterBackgroundTrackTimerEvents addObject:key];
            [self trackTimerPause:key];
        }
    }];
    
    // 停止計時器
    [self stopFlushTimer];
}

第四步:測試驗證


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

-Advertisement-
Play Games
更多相關文章
  • datebase管理 1.創建資料庫-create 語法:create database 資料庫名 character set 編碼 # 註意:預設會存在四個資料庫,其資料庫中存儲的是mysql資料庫伺服器的配置的數據 示例:create database firstDB character set ...
  • 搭建從庫,本質上需要的只是一個一致性備份集及這個備份集對應的位置點信息。之前介紹的幾個備份工具( MySQL中如何選擇合適的備份策略和備份工具 )均可滿足。 這裡,我們重點看看如何基於 XtraBackup 搭建從庫。 整個過程其實比較簡單,無非是備份還原。唯一需要註意的是建立複製時位置點的選擇,包 ...
  • 隨著表中記錄(數據行)的不斷積累,存儲數據逐漸增加,有時我們可能希望計算出這些數據的合計值或者平均值等。 本文介紹如何使用 SQL 語句對錶進行聚合和分組的方法。此外,還介紹在彙總操作時指定條件,以及對彙總結果進行升序、降序的排序方法。 一、對錶進行聚合查詢 本節重點 使用聚合函數對錶中的列進行計算 ...
  • 對實時數據湖的解讀 數據湖的概念是比較寬泛的,不同的人可能有著不同的解讀。這個名詞誕生以來,在不同的階段被賦予了不同的含義。 數據湖的概念最早是在 Hadoop World 大會上提出的。當時的提出者給數據湖賦予了一個非常抽象的含義,他認為它能解決數據集市面臨的一些重要問題。 其中最主要的兩個問題是 ...
  • 來源公眾號:SQL資料庫運維 原文鏈接:https://mp.weixin.qq.com/s?__biz=MzI1NTQyNzg3MQ==&mid=2247485400&idx=1&sn=432b580ed77224bf883db109cb7767b4&chksm=ea3753a8dd40dabed ...
  • 鎖 併發事務可能出現的情況: 讀-讀事務併發:此時是沒有問題的,讀操作不會對記錄又任何影響。 寫-寫事務併發:併發事務相繼對相同的記錄做出改動,因為寫-寫併發可能會產生臟寫的情況,但是沒有一個隔離級別允許臟寫的情況發生。MySQL使用鎖的機制來控制併發情況下讓事務對一條記錄進行排隊修改,只有對記錄修 ...
  • 開心一刻 有個問題一直困擾著我:許仙選擇了救蛇,為什麼楊過卻選擇救雕(而不救蛇) 後面想想,其實楊過救神雕是有原因的,當年神雕和巨蛇打架的時候 雕對楊過說:殺蛇,殺蛇,殺蛇! 蛇對楊過說:殺雕,殺雕,殺雕! 楊過果斷選擇了殺蛇 業務場景 業務描述 業務上有這樣的需求,張三、李四兩個用戶,如果互相關註 ...
  • 華為運動健康服務(HUAWEI Health Kit)允許三方生態應用在獲取用戶授權後,通過REST API介面訪問資料庫,讀取華為和生態伙伴開放的運動健康數據或寫入數據到華為運動健康服務,為用戶提供更加個性化的健康改善服務。如運動類App在獲取授權碼後可以讀取華為用戶的心率、步數等運動數據,最終給 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...