浅沫记忆 发表于 2022-6-6 14:06:49

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 = ;
    if (self) {
      _serverURL = serverURL;
      
      // 创建默认的 session 配置对象
      NSURLSessionConfiguration *configuration = ;
      // 设置当个逐级连接数为 5
      configuration.HTTPMaximumConnectionsPerHost = 5;
      // 设置请求的超时事件
      configuration.timeoutIntervalForRequest = 30;
      // 容许使用蜂窝网络连接
      configuration.allowsCellularAccess = YES;
      
      // 创建一个网络请求回调和完成操作的线程池
      NSOperationQueue *queue = [ init];
      // 设置同步运行的最大操作数为 1, 即个操作FIFO
      queue.maxConcurrentOperationCount = 1;
      // 通过配置对象创建一个 session 对象
      _session = ;
    }
   
    return self;
}
第四步:新增 - buildJSONStringWithEvents:方法,将事件数字转出字符串
/// 将事件数组转换成字符串
- (NSString *)buildJSONStringWithEvents:(NSArray<NSString *> *)events {
    return ", ];
}
第五步:新增 - buildRequestWithJSONString:方法,用于根据 serverURL 和事件字符串来创建 NSURLRequest 请求
- (NSURLRequest *)buildRequestWithJSONString:(NSString *)json {
    // 通过服务器 URL 地址创建请求
    NSMutableURLRequest *request = ;
    // 设置请求的body
    request.HTTPBody = ;
    // 请求方法
    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 = ;
    // 创建请求对象
    NSURLRequest *request = ;
    // 数据上传结果
    __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 = ;
            NSLog(@"Flush events error: %@", desc);
      }
      dispatch_semaphore_signal(flushSemaphore);
    };
   
    // 通过 request 创建请求任务
    NSURLSessionDataTask *task = ;
    // 执行任务
    ;
    // 等待请求完成
    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 = ;
    if (self) {
      _automaticProperties = ;

      // 设置是否需是被动启动标记
      _launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
      
      _loginId = [ objectForKey:SensorsAnalyticsLoginId];
      
      _trackTimer = ;
      
      _enterBackgroundTrackTimerEvents = ;
      
      _fileStroe = [ init];
      
      _database = [ init];
      
      _network = [ initWithServerURL:];
      
      // 添加应用程序状态监听
      ;
    }
    return self;
}
第八步:暴露 - flush 数据上报的方法,并实现。
/// 向服务器同步本地所有数据
- (void)flush;
- (void)flush {
    // 默认向服务端一次发送 50 条数据
    ;
}

- (void)flushByEventCount:(NSUInteger) count {
    // 获取本地数据
    NSArray<NSString *> *events = ;
    // 当本地存储的数据为0或者上传 失败时,直接返回,退出递归调用
    if (events.count == 0 || !) {
      return;
    }
    // 当删除数据失败时,直接返回,退出递归调用,防止死循环
    if () {
      return;
    }
    // 继续删除本地的其他数据
    ;
}
第九步:测试验证
问题描述: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 = ;
    if (self) {
      _automaticProperties = ;

      // 设置是否需是被动启动标记
      _launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
      
      _loginId = [ objectForKey:SensorsAnalyticsLoginId];
      
      _trackTimer = ;
      
      _enterBackgroundTrackTimerEvents = ;
      
      _fileStroe = [ init];
      
      _database = [ init];
      
      _network = [ initWithServerURL:];
      
      // 添加应用程序状态监听
      ;
    }
    return self;
}
第三步:实现 + startWithServerURL: 类方法
static SensorsAnalyticsSDK *sharedInstace = nil;
+ (void)startWithServerURL:(NSString *)urlString {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
      sharedInstace = [ initWithServerURL:urlString];
    });
}
第四步: 修改 + sharedInstance 方法
+ (SensorsAnalyticsSDK *)sharedInstance {
    return sharedInstace;
}
第五步:测试验证
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
   
    ;
    [ 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 = ;
    if (self) {
      _automaticProperties = ;

      // 设置是否需是被动启动标记
      _launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
      
      _loginId = [ objectForKey:SensorsAnalyticsLoginId];
      
      _trackTimer = ;
      
      _enterBackgroundTrackTimerEvents = ;
      
      _fileStroe = [ init];
      
      _database = [ init];
      
      _network = [ initWithServerURL:];
      
      NSString *queueLabel = ;
      _serialQueue = dispatch_queue_create(queueLabel.UTF8String, DISPATCH_QUEUE_SERIAL);
      
      // 添加应用程序状态监听
      ;
    }
    return self;
}
第二步:修改 -flush 方法,并使其在队列中执行
- (void)flush {
    dispatch_async(self.serialQueue, ^{
      // 默认向服务端一次发送 50 条数据
      ;
    });
}
第三步:优化 - track: properties: 方法,并使其在队列中执行
- (void)track:(NSString *)eventName properties:(nullable NSDictionary<NSString *, id> *)properties {
    NSMutableDictionary *event = ;
    // 设置事件 distinct_id 字段,用于唯一标识一个用户
    event[@"distinct_id"] = self.loginId ?: self.anonymousId;
    // 设置事件名称
    event[@"event"] = eventName;
    // 事件发生的时间戳,单位毫秒
    event[@"time"] = ;
   
    NSMutableDictionary *eventProperties = ;
    // 添加预置属性
    ;
    // 添加自定义属性
    ;
    // 判断是否是被动启动状态
    if (self.isLaunchedPassively) {
      eventProperties[@"$app_state"] = @"background";
    }
    // 设置事件属性
    event[@"propeerties"] = eventProperties;
   
    dispatch_async(self.serialQueue, ^{
      // 打印
      ;
    //    ;
      ;
    });
}
2.2 策略一
​客户端本地已经缓存的事件超过一定条数时同步数据。
​实现策略:每次事件触发并入库后,检查一下已缓存的事件条数是否超过了定义的阈值,如果已达到,调用 - flush 方法同步数据。
第一步:添加属性 flushBulkSize ,表示允许本地缓存的事件最大条数
/// 当本地缓存的事件达到最大条数时,上次数据(默认 100 条)
@property (nonatomic, assign) NSInteger flushBulkSize;
第二步:在 - initWithServerURL: 初始化中,初始化化 flushBulkSize 属性,默认值设置为 100 条
- (instancetype)initWithServerURL:(NSString *)urlString {
    self = ;
    if (self) {
      _automaticProperties = ;

      // 设置是否需是被动启动标记
      _launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
      
      _loginId = [ objectForKey:SensorsAnalyticsLoginId];
      
      _trackTimer = ;
      
      _enterBackgroundTrackTimerEvents = ;
      
      _fileStroe = [ init];
      
      _database = [ init];
      
      _network = [ initWithServerURL:];
      
      NSString *queueLabel = ;
      _serialQueue = dispatch_queue_create(queueLabel.UTF8String, DISPATCH_QUEUE_SERIAL);
      
      _flushBulkSize = 100;
      
      // 添加应用程序状态监听
      ;
    }
    return self;
}
第三步:在 - track: properties: 方法,事件入库之后,判断本地缓存的事件条数是否大于 flushBulkSize ,入股大于,则触发数据同步
- (void)track:(NSString *)eventName properties:(nullable NSDictionary<NSString *, id> *)properties {
    NSMutableDictionary *event = ;
    // 设置事件 distinct_id 字段,用于唯一标识一个用户
    event[@"distinct_id"] = self.loginId ?: self.anonymousId;
    // 设置事件名称
    event[@"event"] = eventName;
    // 事件发生的时间戳,单位毫秒
    event[@"time"] = ;
   
    NSMutableDictionary *eventProperties = ;
    // 添加预置属性
    ;
    // 添加自定义属性
    ;
    // 判断是否是被动启动状态
    if (self.isLaunchedPassively) {
      eventProperties[@"$app_state"] = @"background";
    }
    // 设置事件属性
    event[@"propeerties"] = eventProperties;
   
    dispatch_async(self.serialQueue, ^{
      // 打印
      ;
    //    ;
      ;
    });
   
    if (self.database.eventCount >= self.flushBulkSize) {
      ;
    }
}
2.3 策略二
​客户端每隔一定的时间同步一次数据,(比如默认 15 秒)
​实现策略:开启一个定时器,每隔一定时间调用一次 -flush 方法。
第一步:添加 flushInterval 属性,两次数据发送的时间间隔,然后再 - initWithServerURL: 初始化方法中初始化
/// 两次数据发送的时间间隔,单位为秒
@property (nonatomic) NSUInteger flushInterval;

- (instancetype)initWithServerURL:(NSString *)urlString {
    self = ;
    if (self) {
      _automaticProperties = ;

      // 设置是否需是被动启动标记
      _launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
      
      _loginId = [ objectForKey:SensorsAnalyticsLoginId];
      
      _trackTimer = ;
      
      _enterBackgroundTrackTimerEvents = ;
      
      _fileStroe = [ init];
      
      _database = [ init];
      
      _network = [ initWithServerURL:];
      
      NSString *queueLabel = ;
      _serialQueue = dispatch_queue_create(queueLabel.UTF8String, DISPATCH_QUEUE_SERIAL);
      
      _flushBulkSize = 100;
      
      _flushInterval = 15;
      
      // 添加应用程序状态监听
      ;
      
      ;
    }
    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 = ;
    ;
}

- (void)stopFlushTimer {
    ;
    self.flushTimer = nil;
}
第三步:在 - initWithServerURL: 中调用开启定时器方法 - startFlushTimer
- (instancetype)initWithServerURL:(NSString *)urlString {
    self = ;
    if (self) {
      _automaticProperties = ;

      // 设置是否需是被动启动标记
      _launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
      
      _loginId = [ objectForKey:SensorsAnalyticsLoginId];
      
      _trackTimer = ;
      
      _enterBackgroundTrackTimerEvents = ;
      
      _fileStroe = [ init];
      
      _database = [ init];
      
      _network = [ initWithServerURL:];
      
      NSString *queueLabel = ;
      _serialQueue = dispatch_queue_create(queueLabel.UTF8String, DISPATCH_QUEUE_SERIAL);
      
      _flushBulkSize = 100;
      
      _flushInterval = 15;
      
      // 添加应用程序状态监听
      ;
      
      ;
    }
    return self;
}
第四步:实现 - setFlushInterval: 方法
- (void)setFlushInterval:(NSUInteger)flushInterval {
    if (_flushInterval != flushInterval) {
      _flushInterval = flushInterval;
      // 上传本地缓存所有数据
      ;
      // 先暂停定时器
      ;
      // 重新开始定时器
      ;
    }
}
第五步:在 - applicationDidEnterBackground: 方法中停止定时器,在 - applicationDidBecomeActive: 中开启定时器
- (void)applicationDidEnterBackground:(NSNotification *)notification {
    NSLog(@"Application did enter background.");
   
    // 还原标记位
    self.applicationWillResignActive = NO;
   
    // 触发 AppEnd 事件
    // ;
    ;
   
    // 暂停所有事件时长统计
    [self.trackTimer enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSDictionary * _Nonnull obj, BOOL * _Nonnull stop) {
      if (! boolValue]) {
            ;
            ;
      }
    }];
   
    // 停止计时器
    ;
}

- (void)applicationDidBecomeActive:(NSNotification *)notification {
    NSLog(@"Application did enter active.");
   
    // 还原标记位
    if (self.applicationWillResignActive) {
      self.applicationWillResignActive = NO;
      return;
    }
   
    // 将被动启动标记位设置为 NO,正常记录事件
    self.launchedPassively = NO;
   
    // 触发 AppStart 事件
    ;
   
    // 恢复所有的事件时长统计
    for (NSString *event in self.enterBackgroundTrackTimerEvents) {
      ;
    }
    ;
   
    // 开始 $AppEnd 事件计时
    ;
   
    // 开启定时器
    ;
}
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 = ;
    // 当本地存储的数据为0或者上传 失败时,直接返回,退出递归调用
    if (events.count == 0 || !) {
      return;
    }
    // 当删除数据失败时,直接返回,退出递归调用,防止死循环
    if () {
      return;
    }
    // 继续删除本地的其他数据
    ;
}
第二步:- flush 调用修改后的方法
- (void)flush {
    dispatch_async(self.serialQueue, ^{
      // 默认向服务端一次发送 50 条数据
      ;
    });
}
第三步:- applicationDidEnterBackground: 添加后台同步数据的任务
- (void)applicationDidEnterBackground:(NSNotification *)notification {
    NSLog(@"Application did enter background.");
      
    // 还原标记位
    self.applicationWillResignActive = NO;
   
    // 触发 AppEnd 事件
    // ;
    ;
   
    UIApplication *application = UIApplication.sharedApplication;
    // 初始化标记位
    __block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid;
    // 结束后台任务
    void (^endBackgroundTast)(void) = ^() {
      ;
      backgroundTaskIdentifier = UIBackgroundTaskInvalid;
    };
   
    // 标记长时间运行的后台任务
    backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
      endBackgroundTast();
    }];
   
    dispatch_async(self.serialQueue, ^{
      // 发送数据
      ;
      // 结束后台任务
      endBackgroundTast();
    });
   
    // 暂停所有事件时长统计
    [self.trackTimer enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSDictionary * _Nonnull obj, BOOL * _Nonnull stop) {
      if (! boolValue]) {
            ;
            ;
      }
    }];
   
    // 停止计时器
    ;
}
第四步:测试验证


   
   
   
                        

https://www.cnblogs.com/r360/p/16347174.html
页: [1]
查看完整版本: iOS全埋点解决方案-数据同步