一、前言

上一篇《神策分析 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 在执行时可能的几种状态:

  • 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 {    // 是否开启 $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:

由于后者在多个场景都会被调用,而前者仅会在冷启动时被调用,故选择 - 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 的设置方法后再执行,采用如下方案:

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

  • 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,这样就会采集不到冷启动事件。例如:

  • - application:didFinishLaunchingWithOptions: 之后才初始化 SDK;

  • - application:didFinishLaunchingWithOptions: 中异步初始化 SDK;

  • 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,即:

  • - applicationWillEnterForeground:

  • - applicationDidBecomeActive:

对于 SDK 来说,

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

  • UIApplicationWillEnterForegroundNotification

  • UIApplicationDidBecomeActiveNotification

分析如下:

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

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

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/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle?language=objc

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

  1. 神策数据 App 可视化全埋点 2.0 重磅升级!抢先体验

    伴随着大数据计算能力的软硬件环境成熟,国内大部分企业的数据意识在逐渐提升,寄希望于数据高效应用,基于数据驱动的力量推动决策落地.在这个过程中,数据采集作为数据应用的起点,其重要性不言而喻. 目前,神策 ...

  2. $AppStart、$AppEnd 全埋点解析

    对于 $AppStart 和 $AppEnd 而言,归根结底就是判断当前 App 是处于前台还是处于后台.而 Android 系统本身没有给 App 提供相关的接口来判断这些状态,所以我们只能借助其它 ...

  3. iOS手势全埋点:轻拍手势、长按手势、捏合手势、旋转手势、轻扫手势、平移手势、屏幕边缘平移手势

    文章目录 前言 I.手势全埋点方案 1.1 轻拍手势全埋点 1.2 长按手势全埋点 II .右划返回的事件与scrollView滚动事件冲突的解决方案 see also 前言 由于UIGestureR ...

  4. 大咖说:React Native 全埋点实现原理(内附赠书)

    本文主要介绍如何实现 React Native 的全埋点,主要是控件点击 $AppClick 事件.该内容,会默认你有一定的 React Native 开发经验,(若没有,也可参与文末赠书). ■ 作 ...

  5. 白皮书 | 国内首份《Android 全埋点技术白皮书》开源所有项目源码!

    随着大数据行业的快速发展,越来越多的人们意识到--数据基础夯实与否,取决于数据的采集方式. 目前,国内大数据埋点方式多样,按照埋点位置不同,可以分为前端(客户端)埋点与后端(服务器端)埋点.其中全埋点 ...

  6. 重磅!《Android 全埋点技术白皮书》开源所有项目源码!

    随着大数据行业的快速发展,越来越多的人们意识到--数据基础夯实与否,取决于数据的采集方式. 目前,国内大数据埋点方式多样,按照埋点位置不同,可以分为前端(客户端)埋点与后端(服务器端)埋点.其中全埋点 ...

  7. 谷歌分析iOS开发包实战

    这篇文章还可以在这里找到 英语 创建一个成功的产品,你和你的团队必须掌握一个分析和统计用户数据的方法!否则,你永远也无法知道你的应用哪些部分是用户一直偏爱使用的,而且没有发生异常,哪些不是.有许多种方 ...

  8. h5 神策埋点_神策Android全埋点方案分析

    神策Android全埋点方案 原理简单分析: Activity生命周期通过监听Application.ActivityLifecycleCallbacks,fragment的生命周期 及一些点击事件则 ...

  9. 《iOS 全埋点技术白皮书》重磅推出

    数据埋点技术在互联网尤其是移动端上使用非常普遍,全埋点采用"全部采集,按需选取"的形式,对页面中所有交互元素的用户行为进行采集,通过界面配置来决定哪些数据需要进行分析,也被誉为&q ...

  10. 终于!《iOS 全埋点解决方案》正式出版

    全埋点,望而生畏? 有人说,全埋点"最全.最便捷.界面友好.技术门槛低"的数据采集方式,大数据火爆国内对此技术需求迫切,但目前国内外都还没有一本讲解 iOS 全埋点技术相关的书籍. ...

最新文章

  1. 怎么恢复oracle的包,【学习笔记】使用dbms_backup_restore包恢复数据库
  2. 0-安装Vagrant和使用
  3. 安卓项目之微信公众好---初体验
  4. c51转汇编语言窗口,在C51语言中如何嵌入汇编语言
  5. 多维多重背包问题_动态规划--背包问题
  6. 【POJ 3281】Dining【最大匹配、拆点】
  7. NIS 病毒库 更新 地址
  8. GIS相关网站、社区、论坛收藏
  9. Kubernetes 的网络原理 (五)---CNI网络模型
  10. 如何通过网络遥测(Network Telemetry)技术实现精细化网络运维
  11. 计算机桌面不同步,怎样使电脑桌面文件在不同桌面位置上显示
  12. 大数据社会的十三大具体应用场景(二)
  13. java分流什么意思_Flink如何分流数据
  14. 我的阿里云盘资源搜索引擎首次试运行
  15. 为什么数字化时代需要 BizDevOps?
  16. 我学习网络管理员的第一步---各个时期的基本要求
  17. 如何学习数值模拟(一)
  18. Codeforces Round #710 (Div. 3)个人题解
  19. 鸿蒙大陆黑熊在哪,荒野大镖客2传说熊在哪捕获?传说熊捕获位置介绍
  20. 跨境电商趋势报告:五大趋势演变,中国卖家从“野蛮生长”走向“精耕细作”...

热门文章

  1. php递归5,5.5.1 PHP递归函数
  2. 儿童素描手绘创意设计字体 for mac
  3. 超级灰色按钮克星更新v1.3.1112.40
  4. 快播(QvodPlayer)最新版 v5.20.234 官方版
  5. Nexus3 安装 及 配置 docker 私有、代理 仓库
  6. 类似Rainmeter、Conky的安卓插件
  7. 爆强的一句话,工作之余放松放松!
  8. 抽象代数学习笔记(抽象代数的历史、运算)
  9. python分词原理_结巴分词原理
  10. 移动端css文件命名,移动端手机前端css命名规范.docx