草庐IT

iOS全埋点解决方案-应用退出和启动

任伟 2023-03-28 原文

前言

​ 通过应用程序退出事件,可以分析应用程序的平均使用时长;通过应用程序的启动事件,可以分析日活和新增。我们可以通过全埋点方式 SDK 实现应用程序的退出和启动事件。

一、全埋点的简介

​ 目前、全埋点采集可以采集一下4个事件。

1、$AppEnd 事件:应用程序退出事件

2、$AppStart 事件:应用程序启动事件

3、$AppViewScreen 事件: 应用程序内界面预览事件,对于 iOS 来说就是切换不同的 UIViewController。

4、$AppClick 事件: 控件的点击事件,比如点击 UIButton 、UITableView 等。

预置事件:在 SDK 中自动采集的事件称为预置事件。

二、应用程序退出

2.1 应用程序状态:

​ 一个标准的 iOS 程序在不同的时期会有不同的运行状态,在 iOS 程序中常见的状态有5中。如图所示:

1、Not running:非运行状态,指应用程序还没有被启动,或者已经被系统终止。

2、Inactive: 前台非活跃状态,指应用程序即将进入前台状态。

3、Active: 前台活跃状态,指应用程序正在前台运行,可接受事件并进行处理。

4、Background: 进入后台状态,指应用程序进入后台并可执行代码。

5、Suspended: 挂起状态,指应用程序进入后台并没有执行代码,系统会自动将应用程序转移到该状态。挂起时,应用程序会保留在内存中,但不执行任何代码,当系统出现内存不足情况时,系统会清除被挂起的应用程序。

​ 在应用程序的状态转换过程中,系统会调用实现 UIApplicationDelegate 协议类的一些方法,并发送相应的本地通知(先调用方法,待回调方法执行后,再发相应的通知),回调方法和本地通知的对应关系如下表

回调方法 本地通知
- application:didFinishI aunchingWithOptions: UIApplicationDidFinishLaunchingNotification
- applicationDidBecomeActive: UIApplicationDidBecomeActiveNotification
- applicationWillResignActive: UIApplicationWillResignActiveNotification
- applicationDidEnterBack ground:· UIApplicationDidEnterBackgroundNotification
- applicationWillEnterForeground: UIApplicationWillEnterForegroundNotificatio
- applicationWillTerminate: UIApplicationWillTerminateNotification

2.2 实现步骤

​ 通过上面介绍的内容可知,当一个 iOS 应用程序退出时,就意味着该应用程序进入了“后台”,即处于 Background 状态。因此,对于实现 $AppEnd 事件的全埋点,我们只需要注册监听 UIApplicationDidEnterBackgroundNotification 通知,然后在收到通知时触发 $AppEnd 事件,即可达到 $AppEnd 事件全埋点的效果。

第一步:注册监听 UIApplicationDidEnterBackgroundNotification 本地通知。

在 SensorsAnalyticsSDK.m 文件中实现 - setupListeners 方法,用来监听 UIApplicationDidEnterBackgroundNotification 本地通知,然后再相应的回调方法中触发 $AppEnd 事件。

- (void)setupListeners {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    // 注册监听 UIApplicationDidEnterBackgroundNotification 本地通知
    // 当应用程序进入后台,调用通知方法
    [center addObserver:self
               selector:@selector(applicationDidEnterBackground:)
                   name:UIApplicationDidEnterBackgroundNotification
                 object:nil];
}

- (void)applicationDidEnterBackground:(NSNotification *)notification {
    NSLog(@"Application did enter background.");
    
    // 触发 AppEnd 事件
    [self track:@"$AppEnd" properties:nil];
}

第二步:在 SensorsAnalyticsSDK.m 文件中初始化 - init 方法中调用 - setupListeners,并在 - dealloc 方法中移除监听。

- (instancetype)init {
    self = [super init];
    if (self) {
        _automaticProperties = [self collectAutomaticProperties];
        
        // 添加应用程序状态监听
        [self setupListeners];
    }
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

第三步:测试验证

我们可以在 Xcode 中打印控制台中查看如下的打印信息。

{
  "event" : "$AppEnd",
  "time" : 1648520301691,
  "propeerties" : {
    "$model" : "x86_64",
    "$manufacturer" : "Apple",
    "$lib_version" : "1.0.0",
    "$os" : "iOS",
    "$app_version" : "1.0",
    "$os_version" : "15.2",
    "$lib" : "iOS"
  }
}

三、应用程序启动

应用程序的启动,一般情况下,大致可以分为两类场景:

• 冷启动

• 热启动(从后台恢复)

​ 不管是冷启动还是热启动,触发 $AppStart 事件的时机,都可以理解成是当“应用程序开始进入前台并处于活动状态”,也即前文介绍的 Active 状态。因此,为了实现 $AppStart 事件的全埋点,我们可以注册监听 UIApplicationDidBecomeActiveNotification 本地通知,然后在其相应的回调方法里触发 $AppStart 事件。

3.1 实现步骤

第一步:在 SensorsAnalyticsSDK.m 文件 - setupListeners 方法中,添加 UIApplicationDidBecomeActiveNotification 本地通知,然后再相应的回调方法中触发 $AppStart 事件。

- (void)setupListeners {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    // 注册监听 UIApplicationDidBecomeActiveNotification 本地通知
    // 当应用程序进入前台台,调用通知方法
    [center addObserver:self
               selector:@selector(applicationDidBecomeActive:)
                   name:UIApplicationDidBecomeActiveNotification
                 object:nil];
}

- (void)applicationDidBecomeActive:(NSNotification *)notification {
    NSLog(@"Application did enter active.");
    
    // 触发 AppEnd 事件
    [self track:@"$AppStart" properties:nil];
}

第二步: 测试验证

​ 可以在 Xcode 打印控制台中查看下面的打印信息。

{
  "event" : "$AppStart",
  "time" : 1648520708355,
  "propeerties" : {
    "$model" : "x86_64",
    "$manufacturer" : "Apple",
    "$lib_version" : "1.0.0",
    "$os" : "iOS",
    "$app_version" : "1.0",
    "$os_version" : "15.2",
    "$lib" : "iOS"
  }
}

3.2 优化

问题:

通过测试可以发现,仍有以下几个特殊场景存在问题:

• 下拉通知栏并上滑,会触发 $AppStart 事件

• 上滑控制中心并下拉,会触发 $AppStart 事件

• 双击 Home 键进入切换应用程序页面,最后又选择当前应用程序,会触发 $AppStart 事件

以上几个场景均会触发 $AppStart 事件,明显与实际情况有所不符。

那这些现象是什么原因导致的呢?

我们继续分析可以发现以下几个现象:

• 下拉通知栏时,系统会发送 UIApplicationWillResignActiveNotification 通知;上滑通知栏时,系统会发送 UIApplicationDidBecomeActiveNotification 通知

• 上滑控制中心时,系统会发送 UIApplicationWillResignActiveNotification 通知;下拉控制中心时,系统会发送 UIApplicationDidBecomeActiveNotification 通知

• 双击 Home 键进入切换应用程序页面时,系统会发送 UIApplicationWillResignActiveNotification 通知,然后选择当前应用程序,系统会再发送 UIApplicationDidBecomeActiveNotification 通知

很容易总结出规律:在以上几个场景下,系统均是先发送UIApplicationWillResignActiveNotification 通知,然后再发送 UIApplicationDidBecomeActiveNotification 通知。而我们又是通过注册监听 UIApplicationDidBecomeActiveNotification 通知来实现 $AppStart 事件全埋点,因此均会触发 $AppStart 事件。

那如何解决这个问题呢?

在解决这个问题之前,我们先看另一个现象:不管是冷启动还是热启动,系统均没有发送 UIApplicationWillResignActiveNotification 通知。

因此,只要在收到 UIApplicationDidBecomeActiveNotification 通知时,判断之前是否收到过 UIApplicationWillResignActiveNotification 通知,若没有收到,则触发 $AppStart 事件;若已收到,则不触发 $AppStart 事件。这样即可解决上面的问题。

优化方案:

第一步:在 SensorsAnalyticsSDK.m 文件中添加 applicationWillResignActive 标记位。

/// 标记应用程序是否收到 UIApplicationWillResignActiveNotification 本地通知
@property (nonatomic, assign) BOOL applicationWillResignActive;

第二步:在 - setupListeners 方法中新增注册监听 UIApplicationWillResignActiveNotification 的本地通知。

- (void)setupListeners {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    // 注册监听 UIApplicationDidEnterBackgroundNotification 本地通知
    // 当应用程序进入后台,调用通知方法
    [center addObserver:self
               selector:@selector(applicationDidEnterBackground:)
                   name:UIApplicationDidEnterBackgroundNotification
                 object:nil];
    
    // 注册监听 UIApplicationDidBecomeActiveNotification 本地通知
    // 当应用程序进入前台台,调用通知方法
    [center addObserver:self
               selector:@selector(applicationDidBecomeActive:)
                   name:UIApplicationDidBecomeActiveNotification
                 object:nil];
    
    // 注册监听 UIApplicationWillResignActiveNotification 本地通知
    // 当应用程序进入前台台,调用通知方法
    [center addObserver:self
               selector:@selector(applicationWillResignActive:)
                   name:UIApplicationWillResignActiveNotification
                 object:nil];
}

- (void)applicationWillResignActive:(NSNotification *)notification {
    
    // 设置标记位
    self.applicationWillResignActive = YES;
}

第三步:在UIApplicationDidBecomeActiveNotification 的回调方法中还原 applicationWillResignActive 的标记位

- (void)applicationDidBecomeActive:(NSNotification *)notification {
    NSLog(@"Application did enter active.");
    
    // 还原标记位
    if (self.applicationWillResignActive) {
        self.applicationWillResignActive = NO;
        return;
    }
    
    // 触发 AppStart 事件
    [self track:@"$AppStart" properties:nil];
}

第四步:在 UIApplicationDidEnterBackgroundNotification 回调方法中还原 applicationWillResignActive 的标记位

- (void)applicationDidEnterBackground:(NSNotification *)notification {
    NSLog(@"Application did enter background.");
    
    // 还原标记位
    self.applicationWillResignActive = NO;
    
    // 触发 AppEnd 事件
    [self track:@"$AppEnd" properties:nil];
}

第五步:测试验证

{
  "event" : "$AppStart",
  "time" : 1648533646735,
  "propeerties" : {
    "$model" : "x86_64",
    "$manufacturer" : "Apple",
    "$lib_version" : "1.0.0",
    "$os" : "iOS",
    "$app_version" : "1.0",
    "$os_version" : "15.2",
    "$lib" : "iOS"
  }
}

四、应用程序被动启动

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

4.1、Background modes

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

通过上图可知,有如下几种后台运行模式,它们都会触发被动启动($AppStartPassively 事件)。

1、Audio,AirPlay,and Picture in Picture : 音频的播放,录音,AirPlay及画中画的视频播放

2、Location updates:此模式下,会由于地理位置变化而触发应用程序启动

3、Voice over IP : IP网络电话,通过对语音信号进行编码数字化,然后转换成IP数据包在TCP/IP网络上进行传输,从而达到在网络上进行语音通信的目的

4、External Accessory communication:此模式下,一些 MFi 外设通过蓝牙或者 Lightning 接头等方式与 iOS 设备连接,从而可在外设给应用程序发送消息时,触发对应的应用程序启动

5、Uses Bluetooth LE accessories:此模式与 External Accessory communication 类似,只是无需限制 MFi 外设,而需要的是 Bluetooth LE 设备

6、Acts as a Bluetooth LE accessory:此模式下,iPhone 作为一个蓝牙外设连接,可以触发应用程序启动

7、Background fetch:此模式下,iOS 系统会在一定的时间间隔内触发应用程序启动,去获取应用程序数据

8、Remote notifications:此模式是支持静默推送,当应用程序收到这种推送后,不会有任何界面提示,但会触发应用程序启动

9、Background processing: 后端处理

4.2 实现步骤

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

第一步:在 - setupListeners 方法中添加 UIApplicationgDidFinishLaunchingNotification 本地通知,在回调方法中上报数据。

- (void)setupListeners {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    // 注册监听 UIApplicationDidFinishLaunchingNotification 本地通知
    // 当应用程序被动,调用通知方法
    [center addObserver:self
               selector:@selector(applicationDidFinishLaunching:)
                   name:UIApplicationDidFinishLaunchingNotification
                 object:nil];
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
    
    // 触发 AppStartPassively 事件
    [self track:@"$AppStartPassively" properties:nil];
}

第二步:新增一个私有属性 launchedPassively,标记应用程序是否处于被动启动

/// 标记应用程序是否是被动启动
@property (nonatomic, assign, getter=isLaunchedPassively) BOOL launchedPassively;

第三步:在 - init 初始化方法中,通过 backgroundTimeRemaining 属性是否等于 UIApplicationBackgroundFetchIntervalNever 来设置

- (instancetype)init {
    self = [super init];
    if (self) {
        _automaticProperties = [self collectAutomaticProperties];

        // 设置是否需是被动启动标记
        _launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
        
        // 添加应用程序状态监听
        [self setupListeners];
    }
    return self;
}

第四步:在 - applicationDidFinishLaunching 回调方法中,如果 isLaunchedPassively 为 YES,再触发 $AppStartPassively 事件

- (void)applicationDidFinishLaunching:(NSNotification *)notification {
    NSLog(@"Application did finish launching.");
    // 当应用程序后台运行时,触发被动启动事件
    if (self.isLaunchedPassively) {
        // 触发 AppStartPassively 事件
        [self track:@"$AppStartPassively" properties:nil];
    }
}

第五步:测试验证

1、开启 Background modes 中的 Background fetch 复选框

2、选择 Demo Scheme , 一次单击 Xcode 菜单栏中的 Product -> Scheme -> Edit -> Scheme -> Run -> Options

3、勾选 Background Fetch 选项,然后点击 Close 按钮。运行 Demo

{
  "event" : "$AppStartPassively",
  "time" : 1648537321216,
  "propeerties" : {
    "$model" : "x86_64",
    "$manufacturer" : "Apple",
    "$lib_version" : "1.0.0",
    "$os" : "iOS",
    "$app_version" : "1.0",
    "$os_version" : "15.2",
    "$lib" : "iOS"
  }

有关iOS全埋点解决方案-应用退出和启动的更多相关文章

  1. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  2. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  3. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  4. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  5. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  6. ruby-on-rails - 启动 Rails 服务器时 ImageMagick 的警告 - 2

    最近,当我启动我的Rails服务器时,我收到了一长串警告。虽然它不影响我的应用程序,但我想知道如何解决这些警告。我的估计是imagemagick以某种方式被调用了两次?当我在警告前后检查我的git日志时。我想知道如何解决这个问题。-bcrypt-ruby(3.1.2)-better_errors(1.0.1)+bcrypt(3.1.7)+bcrypt-ruby(3.1.5)-bcrypt(>=3.1.3)+better_errors(1.1.0)bcrypt和imagemagick有关系吗?/Users/rbchris/.rbenv/versions/2.0.0-p247/lib/ru

  7. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  8. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  9. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  10. ruby-on-rails - 如何在 Gem 中获取 Rails 应用程序的根目录 - 2

    是否可以在应用程序中包含的gem代码中知道应用程序的Rails文件系统根目录?这是gem来源的示例:moduleMyGemdefself.included(base)putsRails.root#returnnilendendActionController::Base.send:include,MyGem谢谢,抱歉我的英语不好 最佳答案 我发现解决类似问题的解决方案是使用railtie初始化程序包含我的模块。所以,在你的/lib/mygem/railtie.rbmoduleMyGemclassRailtie使用此代码,您的模块将在

随机推荐