自动采集(神策分析iOSSDK3.1.5自动采集功能[2](组图))
优采云 发布时间: 2021-12-16 06:28自动采集(神策分析iOSSDK3.1.5自动采集功能[2](组图))
1. 前言
页面浏览时间用于统计用户在页面停留的时间长度。对于神测分析iOS SDK,在自动页面浏览时间采集功能上线之前,客户通过手动调用开始时间和结束时间的相关接口来实现页面浏览时间采集。这种手动采集方式对客户的业务代码侵入性很大,客户使用成本高。
因此,为了解决上述问题,神测分析iOS SDK3.1.5[1]版本引入了自动页面浏览时间采集功能[2]。该功能不需要用户手动调用界面,可以实现自动采集页面浏览时间。
在实现这个功能的过程中,我们做了很多尝试。下面我们来看看自动采集页面浏览时间的两种方案。
2. 采集方案分析2.1. 场景一
该解决方案主要针对单页情况。采集的原理是:当进入某个页面或应用进入前台时,定时器开始计时;当应用程序返回后台或进入新页面时(这被视为当前页面已经消失)结束计时。
具体的采集逻辑如下:
当收到应用进入前台的通知时,定时器开始计时;
当page-viewDidAppear:的生命周期方法被执行时,触发上一页的关闭事件并记录页面浏览时间,同时启动当前页面的计时;
当收到应用进入后台的通知时,触发当前页面关闭事件,记录页面浏览时间。
优势:
采集逻辑很简单;
业务代码侵入性较小;
埋点成本低;
应用强制查杀可以正常采集页面浏览时间。
缺点:
不支持多页面,不能满足父子页面同时存在时采集的要求;
不支持暂停和恢复计时器。
2.2. 选项二
该解决方案支持单页和多页情况。采集其原理是:定时器在进入页面或应用进入前台时开始计时,在页面消失或应用退出到后台时结束。
具体的采集逻辑如下:
当收到应用进入前台的通知时,定时器开始计时;
当page-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. 钩子页面的生命周期方法
首先需要判断页面浏览时长采集是否开启,如果开启则钩住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. 开始计时
进入新页面时,检查时间戳中是否存在UIViewController的地址(类型为NSMutableDictionary,其中key为UIViewController的地址,value为计时开始时的时间戳):如果存在则忽略; 如果不存在,则替换当前 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中对应的值。如果没有值,直接返回。如果有值,请执行以下步骤:
计算页面浏览时间=当前时间-开始时间;
触发$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. 应用返回后台
当应用退出到后台时,遍历时间戳的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. 应用崩溃
如果想在app崩溃时自动采集页面浏览时间,需要将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:方法,会记录时间戳,但打开应用后会重新计时。所以页面浏览时间是从打开应用到离开页面的时间,从实际情况来看是合理的(毕竟被动启动的时候页面是看不到的);
如果页面被遮挡后没有执行-viewDidDisappear:方法,那么被遮挡的时间也收录在页面浏览时间中。对于这种场景,其实是不合理的,因为页面被屏蔽后就相当于不可见了。因此,有鉴于此,我们会在后续进行优化;
只要执行了-viewDidAppear:方法,我们就会采集。因此,当父子页面同时存在时,每个页面的浏览时间为采集。
总结
本文主要介绍神测如何自动分析iOS SDK采集页面浏览时间。希望大家通过阅读这篇文章,能够清楚的了解如何实现。详情请参考神测分析iOS SDK源码[1]。
目前我们的自动采集页面浏览时间功能还在不断更新迭代中。欢迎大家在开源社区与我们交流。
参考
[1]
[2](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](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