共计 8492 个字符,预计需要花费 22 分钟才能阅读完成。
1. 前言
页面浏览时长是用于统计用户在页面的停留时长。对于神策剖析 iOS SDK 而言,在没有推出页面浏览时长主动采集性能之前,客户是通过手动调用开始计时和完结计时的相干接口实现页面浏览时长采集的。这种手动采集的形式对客户业务代码侵入性大,并且客户应用的老本较高。
因而,为了解决上述问题,神策剖析 iOS SDK 3.1.5[1] 版本推出了页面浏览时长主动采集性能[2]。该性能无需用户手动调用接口,即可实现主动采集页面浏览时长。
在实现此性能的过程中,咱们做了很多尝试,上面先来看一下主动采集页面浏览时长的两种计划。
2. 采集计划剖析
2.1. 计划一
此计划次要是针对单页面的状况,采集原理是:当进入某个页面或者利用进入前台时定时器开始计时;当利用退到后盾或者进入一个新的页面时(此时视为以后页面曾经隐没)完结计时。
具体的采集逻辑如下:
当收到利用进入前台的告诉时,定时器开始计时;
当执行到页面的生命周期办法 – viewDidAppear: 时,触发上一个页面的敞开事件并记录页面浏览时长,同时开始以后页面的计时;
当收到利用进入后盾的告诉时,触发以后页面的敞开事件并记录页面浏览时长。
长处:
采集逻辑简略;
业务代码侵入性小;
埋点成本低;
利用强杀能够失常采集页面浏览时长。
毛病:
不反对多页面,不能满足父子页面同时存在时的采集需要;
不反对暂停和复原计时器。
2.2. 计划二
此计划既反对单页面的状况,也反对多页面的状况。采集原理是:当进入某个页面或者利用进入前台时定时器开始计时,当页面曾经隐没或者利用退到后盾时完结计时。
具体采集逻辑如下:
当收到利用进入前台的告诉时,定时器开始计时;
当执行到页面的生命周期办法 – viewDidAppear: 时,定时器开始计时;
当收到利用进入后盾的告诉时,定时器完结计时,触发以后页面的敞开事件并记录页面浏览时长;
当执行到页面的生命周期办法 – viewDidDisappear: 时,定时器完结计时,触发以后页面的敞开事件并记录页面浏览时长。
长处:
反对多页面;
业务代码侵入性小;
埋点成本低;
利用强杀能够失常采集页面浏览时长。
毛病:
弹出的子页面遮挡了父页面,父页面只有没有执行 – viewDidDisappear: 办法就不会完结计时;
不反对暂停和复原计时器。
2.3. 小结
通过上述剖析,咱们能够晓得两种计划各有利弊。不过计划一不反对多页面的场景,因而最终咱们抉择了计划二作为主动采集页面浏览时长的计划。
3. 具体实现
在介绍主动采集页面浏览时长 [2] 的具体实现之前,咱们先来看下 SDK 生命周期的概念。
3.1. SDK 生命周期
SDK 生命周期是联合了利用的生命周期和 SDK 的外部逻辑,列举了 SDK 须要的状态:
// SDK 生命周期状态
typedef NS_ENUM(NSUInteger, SAAppLifecycleState) {
SAAppLifecycleStateInit,
SAAppLifecycleStateStart, // 利用冷(热)启动
SAAppLifecycleStateStartPassively, // 被动启动[3]
SAAppLifecycleStateEnd, // 退出
SAAppLifecycleStateTerminate, // 终止
};
这样只须要关注 SDK 的状态变动,就能够精确地触发各种事件。例如:SDK 状态变为 SAAppLifecycleStateEnd 阐明利用退出了,此时应该触发页面的敞开事件。代码如下:
- (void)appLifecycleStateWillChange:(NSNotification *)notification {
NSDictionary *userInfo = notification.userInfo;
SAAppLifecycleState newState = [userInfo[kSAAppLifecycleNewStateKey] integerValue];
// 冷(热)启动
if (newState == SAAppLifecycleStateStart) {
// 开始计时
return;
}
// 退出利用
if (newState == SAAppLifecycleStateEnd) {// 完结计时}
}
3.2. 采集流程
如果想要应用主动采集页面浏览时长的性能,只须要将 SAConfigOptions 实例的 enableTrackPageLeave 属性设置为 YES 即可。另外,为了兼容利用解体的场景,在呈现解体时补发页面的敞开事件并记录页面浏览时长。
主动采集页面浏览时长的流程如图 3-1 所示:
图 3-1 主动采集页面浏览时长的流程图
3.3. 外围逻辑
3.3.1. hook 页面的生命周期办法
首先须要判断是否开启了页面浏览时长采集,如果开启就 hook UIViewController 的 – viewDidAppear: 和 – viewDidDisappear: 办法。代码如下所示:
// 判断是否开启页面浏览时长采集
if (!self.configOptions.enableTrackPageLeave) {return;}
// hook viewDidAppear: 和 viewDidDisappear:
[UIViewController sa_swizzleMethod:@selector(viewDidAppear:) withMethod:@selector(sensorsdata_pageLeave_viewDidAppear:) error:NULL];
[UIViewController sa_swizzleMethod:@selector(viewDidDisappear:) withMethod:@selector(sensorsdata_pageLeave_viewDidDisappear:) error:NULL];
3.3.2. 开始计时
当进入一个新的页面,查看 timestamp(类型为 NSMutableDictionary,其中 key 是 UIViewController 的地址,value 蕴含开始计时的工夫戳)中是否存在该 UIViewController 的地址:如果存在,则疏忽;如果不存在,将以后 UIViewController 的地址及以后时刻的工夫戳进行记录。
另外,当利用进入前台时,须要更新 timestamp 里记录的工夫戳为以后工夫。代码如下所示:
// 进入一个新的页面
- (void)trackPageEnter:(UIViewController *)viewController {if (![self shouldTrackViewController:viewController]) {return;}
NSString *address = [NSString stringWithFormat:@"%p", viewController];
// 判断 timestamp 中是否存在该 UIViewController 的地址
if (self.timestamp[address]) {return;}
// 如果不存在,将以后 UIViewController 的地址及该时刻增加到 timestamp 中
NSMutableDictionary *properties = [[NSMutableDictionary alloc] init];
properties[kSAPageLeaveTimestamp] = @([[NSDate date] timeIntervalSince1970]);
properties[kSAPageLeaveAutoTrackProperties] = [self propertiesWithViewController:viewController];
self.timestamp[address] = properties;
}
- (void)appLifecycleStateWillChange:(NSNotification *)notification {
NSDictionary *userInfo = notification.userInfo;
SAAppLifecycleState newState = [userInfo[kSAAppLifecycleNewStateKey] integerValue];
// 冷(热)启动,利用进入前台
if (newState == SAAppLifecycleStateStart) {
// 更新 timestamp 中所有 value 为以后工夫
[self.timestamp enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSMutableDictionary * _Nonnull obj, BOOL * _Nonnull stop) {obj[kSAPageLeaveTimestamp] = @([[NSDate date] timeIntervalSince1970]);
}];
return;
}
}
3.3.3. 完结计时
页面隐没、利用退到后盾、利用解体这三种场景会完结计时,上面咱们来别离看下是如何解决这几种场景的。
3.3.3.1. 页面隐没
当页面隐没时,获取以后 UIViewController 地址,查问 timestamp 中对应的 value。如果没有值,则间接返回。如果有值,执行上面的几个步骤:
计算页面浏览时长 = 以后工夫 – 开始工夫;
触发 $AppPageLeave 事件,并增加属性 event_duration 记录页面浏览时长;
删除 timestamp 中对应的 key-value。
代码如下所示:
// 页面隐没时,判断以后 UIViewController 是否是须要计时的 UIViewController
- (void)trackPageLeave:(UIViewController *)viewController {if (![self shouldTrackViewController:viewController]) {return;}
// 获取以后 UIViewController 的地址,查问 timestamp 中对应的 key-value,NSString *address = [NSString stringWithFormat:@"%p", viewController];
// 如果没有值,则间接返回
if (!self.timestamp[address]) {return;}
// 页面浏览时长 = 以后工夫 - 开始工夫
NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
NSMutableDictionary *properties = self.timestamp[address];
NSNumber *timestamp = properties[kSAPageLeaveTimestamp];
NSTimeInterval startTimestamp = [timestamp doubleValue];
NSMutableDictionary *tempProperties = [[NSMutableDictionary alloc] initWithDictionary:properties[kSAPageLeaveAutoTrackProperties]];
NSTimeInterval duration = (currentTimestamp - startTimestamp) < 24 * 60 * 60 ? (currentTimestamp - startTimestamp) : 0;
tempProperties[kSAEventDurationProperty] = @([[NSString stringWithFormat:@"%.3f", duration] floatValue]);
// 调用触发页面来到事件的办法
[self trackWithProperties:tempProperties];
// 删除 timestamp 对应的 key-value
self.timestamp[address] = nil;
}
// 触发页面来到事件
- (void)trackWithProperties:(NSDictionary *)properties {SAPresetEventObject *object = [[SAPresetEventObject alloc] initWithEventId:kSAEventNameAppPageLeave];
[SensorsAnalyticsSDK.sharedInstance asyncTrackEventObject:object properties:properties];
}
3.3.3.2. 利用退到后盾
当利用退到后盾时,遍历 timestamp 的 key-value,计算页面浏览时长 = 以后工夫 – 开始工夫;而后触发 $AppPageLeave 事件,并增加属性 event_duration 记录页面浏览时长。代码如下所示:
// 利用退到后盾
- (void)appLifecycleStateWillChange:(NSNotification *)notification {
NSDictionary *userInfo = notification.userInfo;
SAAppLifecycleState newState = [userInfo[kSAAppLifecycleNewStateKey] integerValue];
// 利用退出,调用完结计时办法
if (newState == SAAppLifecycleStateEnd) {[self trackEvents];
}
}
// 利用退到后盾时,遍历 timestamp 的 key-value,触发 $AppPageLeave,时长为 currentTimestamp - startTimestamp
- (void)trackEvents {
// 遍历 timestamp 的 key-value
[self.timestamp enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSMutableDictionary * _Nonnull obj, BOOL * _Nonnull stop) {NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
NSNumber *timestamp = obj[kSAPageLeaveTimestamp];
NSTimeInterval startTimestamp = [timestamp doubleValue];
NSMutableDictionary *tempProperties = [[NSMutableDictionary alloc] initWithDictionary:obj[kSAPageLeaveAutoTrackProperties]];
// 计算页面浏览时长
NSTimeInterval duration = (currentTimestamp - startTimestamp) < 24 * 60 * 60 ? (currentTimestamp - startTimestamp) : 0;
tempProperties[kSAEventDurationProperty] = @([[NSString stringWithFormat:@"%.3f", duration] floatValue]);]
// 触发页面来到事件
[self trackWithProperties:[tempProperties copy]];
}];
}
3.3.3.3. 利用解体
如果在利用解体时想要主动采集页面浏览时长,须要将 SAConfigOptions 实例的 enableTrackAppCrash 属性设置为 YES,因为咱们的解体采集是一个独立的模块,须要独自开启。
当利用解体时,遍历 timestamp 的 key-value,计算页面浏览时长 = 以后工夫 – 开始工夫;而后触发 $AppPageLeave 事件,并增加属性 event_duration 记录页面浏览时长。代码如下所示:
// 利用解体
- (void)trackPageLeaveWhenCrashed {if (!self.enable) {return;}
if (!self.configOptions.enableTrackPageLeave) {return;}
[SACommonUtility performBlockOnMainThread:^{if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive) {[self.appPageLeaveTracker trackEvents];
}
}];
}
// 利用解体时,遍历 timestamp 的 key-value,触发 $AppPageLeave,时长为 currentTimestamp - startTimestamp;- (void)trackEvents {
// 遍历 timestamp 的 key-value
[self.timestamp enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSMutableDictionary * _Nonnull obj, BOOL * _Nonnull stop) {NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
NSNumber *timestamp = obj[kSAPageLeaveTimestamp];
NSTimeInterval startTimestamp = [timestamp doubleValue];
NSMutableDictionary *tempProperties = [[NSMutableDictionary alloc] initWithDictionary:obj[kSAPageLeaveAutoTrackProperties]];
// 计算页面浏览时长
NSTimeInterval duration = (currentTimestamp - startTimestamp) < 24 * 60 * 60 ? (currentTimestamp - startTimestamp) : 0;
tempProperties[kSAEventDurationProperty] = @([[NSString stringWithFormat:@"%.3f", duration] floatValue]);]
// 触发页面来到事件
[self trackWithProperties:[tempProperties copy]];
}];
}
3.4. 反对场景
说到这里,大家肯定想晓得目前神策剖析 iOS SDK 反对主动采集哪些场景的页面浏览时长。这里总结了 11 种场景供大家参考,如表 3-1 所示:
表 3-1 反对主动采集页面浏览时长的场景
- 常见问题
对于主动采集页面浏览时长的性能,咱们遇到了一些常见的问题,例如:
被动启动 [3] 是否会影响页面浏览时长采集;
页面被遮挡了是否采集;
父子页面同时存在,如何采集。
上面咱们来看下这些问题的答案:
被动启动时,如果执行 – viewDidAppear: 办法,timestamp 会记录,然而在点开利用后会从新计时。因而页面浏览时长是从点开利用到来到页面的时长,从理论状况上来看也是正当的(毕竟被动启动时页面是看不到的);
如果页面被遮挡后没有执行 – viewDidDisappear: 办法,那么被遮挡的工夫也是计入页面浏览时长里的。对于这种场景,其实是不合理的,因为页面被遮挡后就相当于看不到了。因而针对这一点,咱们后续会进行优化;
只有执行了 – viewDidAppear: 办法,咱们就会采集。因而父子页面同时存在时,会采集各自的页面浏览时长。
- 总结
本文次要介绍神策剖析 iOS SDK 如何主动采集页面浏览时长,心愿大家通过浏览本文可能清晰地理解如何进行实现,更多细节能够参考神策剖析 iOS SDK 源码[1]。
目前咱们的主动采集页面浏览时长性能还在一直地更新迭代,欢送大家在开源社区与咱们进行交换。
- 参考文献
[1]https://github.com/sensorsdat…
[2]https://manual.sensorsdata.cn…(iOS)v1.13-%E9%87%87%E9%9B%86%E9%A1%B5%E9%9D%A2%E6%B5%8F%E8%A7%88%E6%97%B6%E9%95%BF
[3]https://manual.sensorsdata.cn…(iOS)v1.13-App%E8%A2%AB%E5%8A%A8%E5%90%AF%E5%8A%A8($AppStartPassively)%E4%BA%8B%E4%BB%B6%E8%AF%B4%E6%98%8E