iOS全埋点方案中,数据同步的具体实现方式是怎样的?

2026-05-05 16:491阅读0评论SEO教程
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计3749个文字,预计阅读时间需要15分钟。

iOS全埋点方案中,数据同步的具体实现方式是怎样的?

前言:将本地存储的事件数据同步到服务器,然后经过服务端的存储、提取、分析和展示,最终充实发展数据真实的价值。

一、数据同步第一步:在+SensorsSDK项目中,新增SensorsAnalyticsN。

前言

​ 将本地存储的事件数据同步到服务器,然后经过服务端的存储、抽取、分析和展示,充分发挥数据真正的价值。

iOS全埋点方案中,数据同步的具体实现方式是怎样的?

一、数据同步

第一步:在 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]; }

第四步:测试验证

本文共计3749个文字,预计阅读时间需要15分钟。

iOS全埋点方案中,数据同步的具体实现方式是怎样的?

前言:将本地存储的事件数据同步到服务器,然后经过服务端的存储、提取、分析和展示,最终充实发展数据真实的价值。

一、数据同步第一步:在+SensorsSDK项目中,新增SensorsAnalyticsN。

前言

​ 将本地存储的事件数据同步到服务器,然后经过服务端的存储、抽取、分析和展示,充分发挥数据真正的价值。

iOS全埋点方案中,数据同步的具体实现方式是怎样的?

一、数据同步

第一步:在 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]; }

第四步:测试验证