1. 相关概念

在 iOS 7 之后,苹果新增了后台应用程序刷新功能,该功能允许操作系统在一定的时间间隔内(这个时间间隔根据用户不同的操作习惯而有所不同,可能是几个小时,也可能是几天)拉起应用程序并同时让其进入后台运行,以便应用程序可以获取最新的数据并更新相关内容,从而可以确保用户在打开应用程序的时候可以第一时间查看到最新的内容。例如新闻或者社交媒体类型的应用程序,可以使用这个功能在后台获取到最新的数据内容,在用户打开应用程序时可以缩短应用程序启动和获取内容展示的等待时间,最终提升产品的用户体验。

后台应用程序刷新,对于用户来说可以缩短等待时间;对于产品来说,可以提升用户体验;但对于数据采集 SDK 来说,可能会带来一系列的问题。比如,当系统拉起应用程序并同时让其进入后台运行时,应用程序的第一个页面(UIViewController)也会被加载,即会触发一次页面浏览事件,这明显是不合理的,因为用户并没有打开应用程序,更没有浏览第一个页面。其实,整个后台应用程序刷新的过程,对于用户而言,完全是透明的、无感知的。

因此,在实际的数据采集过程中,我们需要避免这种情况的发生,以免影响到正常的数据分析。

我们把应用程序由 iOS 系统触发、自动进入后台运行,称之为(应用程序的)被动启动,通常使用 $AppStartPassively 事件来表示。

后台应用程序刷新是最常见的造成被动启动的原因之一。而后台应用程序刷新只是其中一种后台运行模式,还有一些其他后台运行模式同样也会触发被动启动,下面我们会进行详细介绍。

2. Background Modes

使用 Xcode 创建新的应用程序,默认情况下后台刷新功能是关闭的,我们可以在 Capabilities 标签中开启 Background Modes,然后就可以勾选所需要的功能了,如下图 2-2 所示:

图 2-2 开启 Background Modes

由图 2-2 可知,还有如下几种后台运行模式,它们同样也会导致触发被动启动($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. 采集 $AppStartPassively 事件

后台应用程序刷新拉起应用程序后,首先会回调 AppDelegate 中的 – application:didFinishLaunchingWithOptions: 方法。因此,我们可以通过注册监听 UIApplicationDidFinishLaunchingNotification 本地通知来采集被动启动事件信息。

但是,这里有一个问题:对于应用程序正常的冷启动,也会发送 UIApplicationDidFinishLaunchingNotification 本地通知,导致正常的冷启动也会触发 $AppStartPassively 事件。那如何解决这个问题呢?

我们先来看 UIApplication 类中的一个属性:

@property(nonatomic,readonly) NSTimeInterval backgroundTimeRemaining API_AVAILABLE(ios(4.0));

它表示在应用程序被系统强杀之前,还能继续在后台运行的时间。当应用程序进入前台运行时,backgroundTimeRemaining 的值会被设置为 UIApplicationBackgroundFetchIntervalNever。当应用程序启动时,如果 backgroundTimeRemaining 属性的值不等于 UIApplicationBackgroundFetchIntervalNever,那就意味着此时应用程序是被动启动的。

这样即可解决冷启动也会触发被动启动事件的问题。