一、前言
上一篇《神策剖析 iOS SDK 代码埋点解析》次要介绍了如何设计与实现代码埋点。具体来讲,就是实现了一个 – track: 接口,能够在适合的机会调用,来记录一条用户的行为数据。个别状况下,对于不同的 App,有价值的行为数据是不一样的,调用 – track: 接口的机会天然也是不一样的,须要开发者依据业务场景来手动调用。
对于 App 而言,有些特定的且有剖析意义的用户行为咱们能够在 SDK 间接采集。例如:App 启动、App 退出、元素点击、页面浏览等。为了将其与代码埋点辨别开,咱们称之为全埋点(也叫无埋点、无码埋点、无痕埋点、主动埋点)。
不难看出,全埋点次要面临两个难点:
1、机会:如何在事件产生的机会,插入采集事件的代码?
2、属性:除了默认采集的预置属性外,是否能够采集其余有意义的预置属性?以及如何为这些事件补充自定义属性?
接下来的全埋点解析系列博客,次要就是来解决下面这两个难点。本文次要探讨 App 启动与退出事件的采集。
二、应用程序状态
在探讨 App 启动与退出事件的采集之前,先要理解这两个事件自身的意义。这里须要介绍下 App 的几种运行状态:
typedef
NS_ENUM(NSInteger, UIApplicationState) {
UIApplicationStateActive, UIApplicationStateInactive, UIApplicationStateBackground} API_AVAILABLE(ios(4.0));
App 在执行时可能的几种状态:
1、Active:程序运行在 Foreground,且正在接管事件;
2、Inactive:程序运行在 Foreground,但未接管事件。这可能是因为以下几种起因引起的:中断(例如:传入电话或 SMS 音讯)、利用正在过渡到后盾、利用从后盾过渡而来;
3、Background:程序运行在 Background,且正在执行代码。
此外,App 还会有两种没有执行代码的状态:
1、Not Running:程序未运行。App 首次装置还未启动、App 被 Kill、手机重启后还未运行 App 等均会处于此状态;
2、Suspended:程序运行在 Background,但没有执行代码,处于挂起状态。大部分利用进入后盾,都会在短暂工夫内被零碎切换为挂起状态。
这五个状态即为 App 所有的运行状态,如图 2-1 所示:
图 2-1 App 运行状态(图片来源于 Apple 开发者官网)
当应用程序的运行状态发生变化时,会回调 UIApplicationDelegate 中的协定办法,默认是由 AppDelegate 实现的,如表 2-1 所示:
表 2-1 UIApplicationDelegate 中的协定办法
这里须要留神的是,并不是每一种状态变动都会有对应的办法,如图 2-1 中红框内的两个变动就没有对应的办法。
App 启动与退出事件的采集,该当在这些办法与告诉中寻找思路。上面列举下常见的运行状态变动的场景:
1、冷启动,也即 Kill App 之后启动,或 App 装置后第一次启动(Not Running -> Inactive -> Active);
2、App 返回主屏幕(Active -> Inactive -> Background -> Suspended)。若在 Info.plist 中设置 Application does not run in background 为 YES,则 App 返回主屏幕后会立刻被 Kill(Active -> Inactive -> Background -> Suspended -> Not Running);
3、App 内进入 App 切换器,而后间接返回 App(Active -> Inactive -> Active);
4、App 内进入 App 切换器,而后进入主屏幕(Active -> Inactive -> Background -> Suspended);
5、App 内进入 App 切换器,而后 Kill App(Active -> Inactive -> Background -> Suspended -> Not Running);
6、App 挂起状态从新运行,即热启动(Suspended -> Background -> Inactive -> Active);
7、App 挂起状态时 Kill App 或间接删除 App(Suspended -> Not Running)。
三、App 启动
App 启动是指应用程序启动,同时包含冷启动和热启动场景。冷启动与热启动会波及到不同的 App 利用状态办法,因而采集形式也是不雷同的,上面离开探讨。
3.1 冷启动
3.1.1 采集计划
冷启动,即 Kill App 之后启动,或 App 装置后第一次启动。采集办法如下:
- (void)autoTrackAppStart {// 是否开启 $AppStart 全埋点 if
([self isAutoTrackEventTypeIgnored:SensorsAnalyticsEventTypeAppStart]) {
return; } // 因为一次残缺的利用生命周期只会触发一次冷启动,因而增加 dispatch_once 以避免屡次触发。static
dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ // 是否首次启动,记录到 SA_EVENT_PROPERTY_APP_FIRST_START 中。标记存到 NSUserDefaults 中。BOOL
isFirstStart = NO;
if
(![[NSUserDefaults standardUserDefaults] boolForKey:SA_HAS_LAUNCHED_ONCE]) {
isFirstStart = YES; [[NSUserDefaults standardUserDefaults] setBool:YES forKey:SA_HAS_LAUNCHED_ONCE]; } // 判断是不是被动启动。被动启动会记录到 App 被动启动事件中。前面会细讲 App 被动启动。NSString eventName = [self isLaunchedPassively] ? SA_EVENT_NAME_APP_START_PASSIVELY : SA_EVENT_NAME_APP_START; // SA_EVENT_PROPERTY_RESUME_FROM_BACKGROUND:App 是否从后盾复原。以此辨别冷热启动。NSDictionary properties = @{SA_EVENT_PROPERTY_RESUME_FROM_BACKGROUND: @(NO), SA_EVENT_PROPERTY_APP_FIRST_START: @(isFirstStart)}; [self track:eventName withProperties:properties withTrackType:SensorsAnalyticsTrackTypeAuto]; // 启动 $AppEnd 事件计时器 [self trackTimerStart:SA_EVENT_NAME_APP_END]; });}
接下来须要解决的问题,就是采集机会。从上面对 App 利用状态的形容中晓得,冷启动的过程中 App 会经验 Not Running -> Inactive -> Active 这一流程,也即执行如下两个办法:
- application:didFinishLaunchingWithOptions:
- applicationDidBecomeActive:
因为 – applicationDidBecomeActive: 在多个场景都会被调用,而 – application:didFinishLaunchingWithOptions: 仅会在冷启动时被调用,故抉择在 – application:didFinishLaunchingWithOptions: 办法中采集冷启动事件即可。代码如下:- (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions {[self autoTrackAppStart]; return
YES;
}
3.1.2 计划优化
因为咱们设计的是 SDK,那么代码就应该实现高内聚低耦合的指标。因而,须要对下面的代码进行一些革新:
1、- autoTrackAppStart 应为 SDK 的办法,它会依赖 SDK 的初始化;
2、SDK 有一些设置办法应在采集第一个事件之前设置好,例如:设置公共属性等。
于是,代码革新如下:
- (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions {// 初始化 SDK [SensorsAnalyticsSDK startWithConfigOptions:nil]; // 初始化公共属性 [[SensorsAnalyticsSDK sharedInstance] registerSuperProperties:{@”key”:@”value”}]; // 采集冷启动 [[SensorsAnalyticsSDK sharedInstance] autoTrackAppStart]; return
YES;
}
上述代码实现了 SDK 的高级指标,但仍存在一些须要改良的问题:
1、代码之间有较严格的执行程序要求,例如:- autoTrackAppStart 必须放到 – registerSuperProperties: 前面,这无形中给开发者减少了集成难度;
2、- autoTrackAppStart 作为一个采集冷启动事件的办法,不应裸露在 SDK 外。它作为全埋点的一部分,对外裸露一个设置全埋点的接口即可;
3、因为全埋点波及到监听系统的办法,所以目前咱们心愿将“设置全埋点类型”作为一个 SDK 的初始化参数,不倡议在初始化后再去设置或批改。
基于以上起因,咱们新增了 SAConfigOptions 这个类,用于配置 SDK 的初始化参数。在 SAConfigOptions 中设置 autoTrackEventType 属性,用于设置全埋点属性,如下所示:
- (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions {// 配置 SDK 初始化参数;SERVER_URL 是数据接管地址 SAConfigOptions *options = [SAConfigOptions.alloc initWithServerURL:SA_SERVER_URL launchOptions:launchOptions]; // 配置开启全埋点:冷启动 [options setAutoTrackEventType:SensorsAnalyticsEventTypeAppStart]; // 初始化 SDK [SensorsAnalyticsSDK startWithConfigOptions:options]; // 初始化公共属性 [[SensorsAnalyticsSDK sharedInstance] registerSuperProperties:{@”key”:@”value”}]; return
YES;
}
为了确保 – autoTrackAppStart 办法能够在 – registerSuperProperties: 及其他 SDK 的设置办法后再执行,采纳如下计划:
1、在 SDK 初始化办法 + startWithConfigOptions: 中监听 UIApplicationDidFinishLaunchingNotification 告诉,该告诉会在 – didFinishLaunchingWithOptions: 执行结束后收回;
2、在监听到 UIApplicationDidFinishLaunchingNotification 之后,调用 – autoTrackAppStart 办法。
因而,SDK 的初始化办法设计如下(代码中包含所有已提到的 SDK 须要初始化的内容):
- (void)startWithConfigOptions:(SAConfigOptions )configOptions {NSAssert(sensorsdata_is_same_queue(dispatch_get_main_queue()), @” 神策 iOS SDK 必须在主线程里进行初始化,否则会引发无奈意料的问题(比方失落 $AppStart 事件)。”); dispatch_once(&sdkInitializeOnceToken, ^{ sharedInstance = [[SensorsAnalyticsSDK alloc] initWithConfigOptions:configOptions debugMode:SensorsAnalyticsDebugOff]; });}- (instancetype)initWithConfigOptions:(nonnull SAConfigOptions )configOptions debugMode:(SensorsAnalyticsDebugMode)debugMode {@try
{
self = [super init]; if
(self) {
_configOptions = [configOptions copy]; dispatch_block_t mainThreadBlock = ^(){ // 判断被动启动 if
(UIApplication.sharedApplication.applicationState == UIApplicationStateBackground) {
self->_launchedPassively = YES; } }; sensorsdata_dispatch_main_safe_sync(mainThreadBlock); // Debug 模式 _debugMode = debugMode; // 数据接管地址 _network = [[SANetwork alloc] initWithServerURL:[NSURL URLWithString:_configOptions.serverURL]]; // 是否为热启动 _appRelaunched = NO; // 避免发反复的 App 退出事件 _applicationWillResignActive = NO; // 计时器 _trackTimer = [[SATrackTimer alloc] init]; // 取上一次过程退出时保留的 distinctId、loginId、superProperties [self unarchive]; // 是否首日拜访 if
(self.firstDay == nil) {
NSDateFormatter dateFormatter = [SADateFormatter dateFormatterFromString:@”yyyy-MM-dd”]; self.firstDay = [dateFormatter stringFromDate:[NSDate date]]; [self archiveFirstDay]; } // 收集预置属性 self.automaticProperties = [self collectAutomaticProperties]; // 监听告诉 [self setUpListeners]; } } @catch(NSException exception) {SAError(@”%@ error: %@”, self, exception); } return
self;
}- (void)setUpListeners {// 监听 App 启动或完结事件 NSNotificationCenter notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(applicationDidFinishLaunching:) name:UIApplicationDidFinishLaunchingNotification object:nil]; [notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; [notificationCenter addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; [notificationCenter addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; [notificationCenter addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];}- (void)applicationDidFinishLaunching:(NSNotification )notification {// 采集冷启动事件 [self autoTrackAppStart];}
到目前为止,该计划还存在一个问题:开发者在集成 SDK 时,有可能在 UIApplicationDidFinishLaunchingNotification 告诉收回后才初始化 SDK,这样就会采集不到冷启动事件。例如:
1、在 – application:didFinishLaunchingWithOptions: 之后才初始化 SDK;
2、在 – application:didFinishLaunchingWithOptions: 中异步初始化 SDK;
3、App 启动时先网络申请数据接管地址,而后再初始化 SDK。
为了防止这些失落冷启动事件的状况,可采取如下的解决方案:在 SDK 初始化时(即 – initWithConfigOptions:debugMode: 中),在主线程中异步再调用一次 – autoTrackAppStart 办法。该计划有以下长处:
1、因为 – autoTrackAppStart 办法中有 dispatch_once 保障代码只执行一次,因而不会反复触发冷启动事件;
2、若 App 已错过 UIApplicationDidFinishLaunchingNotification 告诉,则该异步办法可保障仍会采集到冷启动事件;
3、主线程异步工作会在 SDK 相干初始化实现后才会执行,此时采集的冷启动事件不会失落公共属性。
代码如下:
- (instancetype)initWithConfigOptions:(nonnull SAConfigOptions *)configOptions debugMode:(SensorsAnalyticsDebugMode)debugMode {…… dispatch_async(dispatch_get_main_queue(), ^{[self autoTrackAppStart]; }); ……..}
不过该计划也存在毛病:只实用于在主线程初始化 SDK,无奈解决在子线程初始化 SDK 的问题。因而,须要保障 SDK 必须在主线程中初始化,这也是 + startWithConfigOptions: 办法中增加判断主线程断言的起因。
3.2 热启动
热启动,即 App 处于 Supspended 状态下,从主屏幕进入 App 内的状况。采集办法如下:
- (void)trackRelaunchAppStart {// 追踪 AppStart 事件 if
([self isAutoTrackEventTypeIgnored:SensorsAnalyticsEventTypeAppStart] == NO) {
[self track:SA_EVENT_NAME_APP_START withProperties:@{SA_EVENT_PROPERTY_RESUME_FROM_BACKGROUND: @(_appRelaunched), SA_EVENT_PROPERTY_APP_FIRST_START: @(NO),} withTrackType:SensorsAnalyticsTrackTypeAuto]; } // 启动 $AppEnd 事件计时器 [self trackTimerStart:SA_EVENT_NAME_APP_END];}
由后面的剖析可知,此时的状态变动为:Suspended -> Background -> Inactive -> Active,即:
1、- applicationWillEnterForeground:
2、- applicationDidBecomeActive:
对于 SDK 来说,绝对 UIApplicationDelegate 的协定办法,监听告诉更为不便。因而,理论应用的是以下两个告诉:
1、UIApplicationWillEnterForegroundNotification
2、UIApplicationDidBecomeActiveNotification
剖析如下:
1、有许多状况下会呈现 Inactive -> Active,故不可独自应用 UIApplicationDidBecomeActiveNotification;
2、发送 UIApplicationWillEnterForegroundNotification 时,appState 仍为 Background,有些代码执行可能有问题。
因而,须要综合应用下面两个告诉,代码如下:
- (void)applicationWillEnterForeground:(NSNotification )notification {// 标识符,告诉 SDK 在接下来的告诉 UIApplicationDidBecomeActiveNotification 中采集热启动事件 _appRelaunched = YES; // 热启动,非被动启动 self.launchedPassively = NO;}- (void)applicationDidBecomeActive:(NSNotification )notification {// 非由后盾进入前台,间接返回 if
(!_appRelaunched) {
return; } _appRelaunched = NO; // 追踪 AppStart 事件 [self trackRelaunchAppStart];}
3.3 被动启动
3.3.1 相干概念
在 iOS 7 之后,苹果新增了后盾应用程序刷新性能,该性能容许操作系统在肯定的工夫距离内(这个工夫距离依据用户不同的操作习惯而有所不同,可能是几个小时,也可能是几天)拉起应用程序并同时让其进入后盾运行,以便应用程序能够获取最新的数据并更新相干内容,从而能够确保用户在关上应用程序的时候能够第一工夫查看到最新的内容。例如:新闻或者社交媒体类型的应用程序,能够应用这个性能在后盾获取到最新的数据内容,在用户关上应用程序时能够缩短应用程序启动和获取内容展现的等待时间,最终晋升产品的用户体验。
后盾应用程序刷新,对于用户来说能够缩短等待时间;对于产品来说,能够晋升用户体验;但对于数据采集 SDK 来说,可能会带来一系列的问题。例如:当零碎拉起应用程序并同时让其进入后盾运行时,应用程序的第一个页面(UIViewController)也会被加载,即会触发一次页面浏览事件,这显著是不合理的,因为用户并没有关上应用程序,更没有浏览第一个页面。其实,整个后盾应用程序刷新的过程,对于用户而言,齐全是通明的、无感知的。因而,在理论的数据采集过程中,咱们须要防止这种状况的产生,免得影响到失常的数据分析。
这里咱们把应用程序由 iOS 零碎触发、主动进入后盾运行,称之为(应用程序的)被动启动,通常应用 $AppStartPassively 事件来示意。后盾应用程序刷新是最常见的造成被动启动的起因之一,而后盾应用程序刷新只是其中一种后盾运行模式,还有一些其余后盾运行模式同样也会触发被动启动,上面咱们会进行具体介绍。
3.3.2 Background Modes
应用 Xcode 创立新的应用程序,默认状况下后盾刷新性能是敞开的,咱们能够在 Capabilities 标签中开启 Background Modes,而后就能够勾选所须要的性能了,如图 3-1 所示:
图 3-1 Background Modes
由图 3-1 可知,还有如下几种后盾运行模式,它们同样也会导致触发被动启动($AppStartPassively 事件)。
Location updates:此模式下,因为地理位置变动而触发应用程序启动;
Newsstand downloads:该模式只针对报刊杂志类应用程序,当有新的报刊可下载时,会触发应用程序启动;
External Accessory communication:该模式下,一些 MFi 外设通过蓝牙或者 Lightning 接头等形式与 iOS 设施连贯,从而可在外设给应用程序发送音讯时,触发对应的应用程序启动;
Uses Bluetooth LE accessories:该模式与 External Accessory communication 相似,只是无需限度 MFi 外设,而须要的是 Bluetooth LE 设施;
Acts as a Bluetooth LE accessory:该模式下,iPhone 作为一个蓝牙外设连贯,能够触发应用程序启动;
Background fetch:该模式下,iOS 零碎会在肯定的工夫距离内触发应用程序启动,去获取应用程序数据;
Remote notifications:该模式是反对静默推送,当应用程序收到这种推送后,不会有任何界面提醒,但会触发应用程序启动。
3.3.3 采集计划
后盾应用程序刷新拉起应用程序后,首先会回调 AppDelegate 中的 – application:didFinishLaunchingWithOptions: 办法。因而,咱们能够通过注册监听 UIApplicationDidFinishLaunchingNotification 本地告诉来采集被动启动事件信息。
然而,这里有一个问题:对于应用程序失常的冷启动,也会发送 UIApplicationDidFinishLaunchingNotification 本地告诉,导致失常的冷启动也会触发 $AppStartPassively 事件。那如何解决这个问题呢?
还是要通过第二节中探讨的 UIApplication 的 applicationState 来决定:
@property(nonatomic,readonly) UIApplicationState applicationState API_AVAILABLE(ios(4.0));
失常冷启动,applicationState 的值应该为 UIApplicationStateInactive;但若是被动启动,则该值会是 UIApplicationStateBackground。因而,当应用程序启动时,如果 applicationState 属性的值等于 UIApplicationStateBackground,那就意味着此时应用程序是被动启动的。这样即可解决冷启动也会触发被动启动事件的问题,代码如下:
- (instancetype)initWithConfigOptions:(nonnull SAConfigOptions *)configOptions debugMode:(SensorsAnalyticsDebugMode)debugMode {…… dispatch_block_t mainThreadBlock = ^(){// 判断被动启动 if
(UIApplication.sharedApplication.applicationState == UIApplicationStateBackground) {
self->_launchedPassively = YES; } }; ……}
四、App 退出
通过之前介绍的内容可知,当一个 App 退出(应用 $AppEnd 事件示意),就意味着该应用程序进入了“后盾”,即处于 Background 状态。因而,对于实现 $AppEnd 事件的全埋点,咱们只须要注册监听 UIApplicationDidEnterBackgroundNotification 告诉,而后在收到告诉时触发 $AppEnd 事件,即可达到 $AppEnd 事件全埋点的成果。
4.1 采集计划
App 退出,分为以下几种状况:
1、App 内返回主屏幕;
2、App 进入切换器,而后返回主屏幕;
3、App 进入切换器,而后 Kill App。
无论哪种状况,都会执行 Active -> Inactive -> Background -> Suspended 这一状态转换。因而,该当思考在以下两个告诉之一记录该事件:
UIApplicationWillResignActiveNotification
UIApplicationDidEnterBackgroundNotification
同样,因为有许多状况下会呈现 Active -> Inactive,因而不可独自应用 UIApplicationWillResignActiveNotification,代码如下:
- (void)applicationDidEnterBackground:(NSNotification )notification {// 重置 “ 是否被动启动 ” 标记 self.launchedPassively = NO; // 设置后台任务超时 block,若收到告诉时完结后台任务 UIApplication application = UIApplication.sharedApplication; __block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid; backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{ [application endBackgroundTask:backgroundTaskIdentifier]; backgroundTaskIdentifier = UIBackgroundTaskInvalid; }]; // 追踪 $AppEnd 事件 if
([self isAutoTrackEventTypeIgnored:SensorsAnalyticsEventTypeAppEnd] == NO) {
[self track:SA_EVENT_NAME_APP_END withTrackType:SensorsAnalyticsTrackTypeAuto]; }}
这里须要阐明的是,App 退出事件附带的 App 浏览时长属性的采集形式如下:
1、无论冷热启动触发时,咱们都记录了 App 启动时的工夫,并将其值存储起来(key 为 $AppEnd);
2、待采集 App 退出事件时,咱们会用 $AppEnd 为 key 取出之前预存的 App 启动工夫,与以后工夫相减,得出的时长即为 App 浏览时长,存储到 $event_duration 属性中。
4.2 计划优化
上述计划存在一个问题:一些非凡状况下,application 会间断收回两次 UIApplicationDidEnterBackgroundNotification 告诉。例如:回到主屏幕的同时进行锁屏,就会呈现收回一次 UIApplicationWillResignActiveNotification 告诉之后,又收回两次 UIApplicationDidEnterBackgroundNotification 告诉。这样会采集到多余的退出事件,且第二次事件不会有 App 浏览时长,这显然是不失常的。
剖析出了景象,解决方案也就跃然纸上了:在 App 进入后台前,会发送 UIApplicationWillResignActiveNotification 告诉,此时用一个标记位示意利用登记了 Active 状态;当 App 进入后盾时,会收到 UIApplicationDidEnterBackgroundNotification 告诉,并将此标记变成 NO。因而,通过判断此标记为是否为 YES 即可,代码如下:
- (void)applicationWillResignActive:(NSNotification )notification {_applicationWillResignActive = YES;}- (void)applicationDidEnterBackground:(NSNotification )notification {if
(!_applicationWillResignActive) {
return; } _applicationWillResignActive = NO; ……}
五、总结
本文是系列博客《神策剖析 iOS SDK 源码解析》的第二篇,次要介绍对于 iOS 全埋点中启动与退出埋点的设计方案,以及 App 利用状态的相干常识。心愿能为大家在学习全埋点技术的路线上带来一些帮忙。
六、参考文献
Managing Your App’s Life Cycle(https://developer.apple.com/d…)
文章起源:公众号神策技术社区