神策分析 iOS SDK 全埋点解析之启动与退出 | 数据采集

一、前言

上一篇《神策分析 iOS SDK 代码埋点解析》主要介绍了如何设计与实现代码埋点。具体而言,就是实现了一个 - track: 接口,可以在合适的时机调用,来记录一条用户的行为数据。一般情况下,对于不同的 App,有价值的行为数据是不一样的,调用 - track: 接口的时机自然也是不一样的,需要开发者根据业务场景来手动调用。

对于 App 而言,有些特定的且有分析意义的用户行为我们可以在 SDK 直接采集。例如:App 启动、App 退出、元素点击、页面浏览等。为了将其与代码埋点区分开,我们称之为全埋点(也叫无埋点、无码埋点、无痕埋点、自动埋点)。

不难看出,全埋点主要面临两个难点:

1、时机:如何在事件发生的时机,插入采集事件的代码?

2、属性:除了默认采集的预置属性外,是否可以采集其他有意义的预置属性?以及如何为这些事件补充自定义属性?

接下来的全埋点解析系列博客,主要就是来解决上面这两个难点。本文主要讨论 App 启动与退出事件的采集。

二、应用程序状态

在讨论 App 启动与退出事件的采集之前,先要了解这两个事件本身的意义。这里需要介绍下 App 的几种运行状态:

typedef NS_ENUM(NSInteger, UIApplicationState) {

App 在执行时可能的几种状态:

  • Active:程序运行在 Foreground,且正在接收事件;

  • Inactive:程序运行在 Foreground,但未接收事件。这可能是由于以下几种原因引起的:中断(例如:传入电话或SMS消息)、应用正在过渡到后台、应用从后台过渡而来;

  • Background:程序运行在 Background,且正在执行代码。

此外,App 还会有两种没有执行代码的状态:

  • Not Running:程序未运行。App 首次安装还未启动、App 被 Kill、手机重启后还未运行 App 等均会处于此状态;

  • 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 {

接下来需要解决的问题,就是采集时机。从上面对 App 应用状态的描述中知道,冷启动的过程中 App 会经历 Not Running -> Inactive -> Active 这一流程,也即执行如下两个方法:

1、- applicationWillEnterForeground:
2、- applicationDidBecomeActive:

由于后者在多个场景都会被调用,而前者仅会在冷启动时被调用,故选择 - application:didFinishLaunchingWithOptions: 方法中采集冷启动事件即可。代码如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

3.1.2 方案优化

由于我们设计的是 SDK,那么代码就应该实现高内聚低耦合的目标。因此,需要对上面的代码进行一些改造:

1、- autoTrackAppStart 应为 SDK 的方法,它会依赖 SDK 的初始化;

2、SDK 有一些设置方法应在采集第一个事件之前设置好,例如:设置公共属性等。

于是,代码改造如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

上述代码实现了 SDK 的初级目标,但仍存在一些需要改进的问题:

1、代码之间有较严格的执行顺序要求,例如:- autoTrackAppStart 要放到 - registerSuperProperties: 后面,这无形中给开发者增加了集成难度;

2、- autoTrackAppStart 作为一个采集冷启动事件的方法,不应暴露在 SDK 外。它作为全埋点的一部分,对外暴露一个设置全埋点的接口即可;

3、因为全埋点涉及到监听系统的方法,所以目前我们希望将 “设置全埋点类型” 作为一个 SDK 的初始化参数,不建议在初始化后再去设置或修改。

基于以上原因新增了 SAConfigOptions 这个类,用于配置 SDK 的初始化参数。

在 SAConfigOptions 中,设置了 autoTrackEventType 属性,用于设置全埋点属性,如下所示:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

为了确保 - autoTrackAppStart 方法可以在 - registerSuperProperties: 及其他 SDK 的设置方法后再执行,采用如下方案:

  • 在 SDK 初始化方法中监听UIApplicationDidFinishLaunchingNotification 通知,该通知会在- didFinishLaunchingWithOptions: 执行完毕后发出;

  • UIApplicationDidFinishLaunchingNotification 被监听到之后,调用- autoTrackAppStart 方法。

因此,SDK 的初始化方法设计如下(代码中包括所有已提到的 SDK 需要初始化的内容):

+ (void)startWithConfigOptions:(SAConfigOptions *)configOptions {

到目前为止,该方案还存在一个问题:开发者在集成 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 {

不过该方案也存在缺点:只适用于在主线程初始化 SDK ,无法解决在子线程初始化 SDK 的问题。因此,需要保证 SDK 必须在主线程中初始化,这也是 + startWithConfigOptions: 方法中添加判断主线程断言的原因。

3.2 热启动

热启动,即 App 处于 Supspended 状态下,从主屏幕进入 App 内的情况。采集方法如下:

- (void)trackRelaunchAppStart {

由前面的分析可知,此时的状态变化为:Suspended -> Background -> Inactive -> Active,即:

1、- applicationWillEnterForeground:
2、- applicationDidBecomeActive:

对于 SDK 来说,相对 UIApplicationDelegate 的协议方法,监听通知更为方便。因此,实际使用的是以下两个通知:

  • UIApplicationWillEnterForegroundNotification

  • UIApplicationDidBecomeActiveNotification

分析如下:

  • 有许多情况下会出现 Inactive -> Active,故不可单独使用UIApplicationDidBecomeActiveNotification;

  • UIApplicationWillEnterForegroundNotification 发送时,appState 仍为 Background,有些代码执行可能有问题。

因此,需要综合使用上面两个通知,代码如下:

- (void)applicationWillEnterForeground:(NSNotification *)notification {

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;

applicationState 被动启动的值则是 UIApplicationStateBackground。

因此,当应用程序启动时,出现后者的情况,那就意味着此时应用程序是被动启动的。这样即可解决冷启动也会触发被动启动事件的问题,代码如下:

- (instancetype)initWithConfigOptions:(nonnull SAConfigOptions *)configOptions debugMode:(SensorsAnalyticsDebugMode)debugMode {

四、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 {

这里需要说明的是,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 {

五、总结

本文是系列博客《神策分析 iOS SDK 源码解析》的第二篇,主要介绍关于 iOS 全埋点中启动与退出埋点的设计方案,以及 App 应用状态的相关知识。希望能为大家在学习全埋点技术的道路上带来一些帮助。

参考文献:

Managing Your App's Life Cycle:

https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle?language=objc

本文作者

张占凯 神策数据 | SDK 技术顾问

我是张占凯,热爱技术,目前专注于 iOS 领域。工作之余喜好读书,对各个领域的知识都有所涉猎。希望与大家在开源社区中共同探讨,共同进步。

本文著作权归「神策数据开源社区」所有,商业转载请联系我们获得授权;非商业转载请注明出处,并附上神策数据开源社区服务号二维码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351