
将本地存储的事件数据同步到服务器,然后经过服务端的存储、抽取、分析和展示,充分发挥数据真正的价值。
第一步:在 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 的难度和成本,另一方面是为了确保数据的正确性、完整性和及时性。
因为事件和事件之间是有先后顺序的,因此,在同步数据的时候,需要严格按照事件触发的时间吸纳后顺序同步数据。所以我们需要先优化 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];
});
}
客户端本地已经缓存的事件超过一定条数时同步数据。
实现策略:每次事件触发并入库后,检查一下已缓存的事件条数是否超过了定义的阈值,如果已达到,调用 - 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];
}
}
客户端每隔一定的时间同步一次数据,(比如默认 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];
}
应用程序进入后天时尝试不同本地已缓存的所有数据
实现策略:通过 - 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];
}
第四步:测试验证
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',
这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的
我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声
首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有, 也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit